This repository contains everything needed to build and deploy OpenHEXA static webapps that let users trigger SNT pipeline runs from a browser β no code required.
The active product is the SNT Pipelines Orchestrator β one webapp per workspace that presents
all official SNT pipelines (~18, from the snt_development repo) as a single guided surface with
a configuration/run sidebar. It ships in two UI variants: flowchart (an interactive 2D
node/edge map) and cockpit (a one-step-at-a-time guided walkthrough, and the variant where new
functionality lands first). The earlier single-pipeline webapps were the stepping stones toward it
and now live in archive/ for reference only.
Webapps call the OpenHEXA GraphQL API via a same-origin proxy to start pipeline runs and poll
their status. A simple webapp can be a single index.html; the orchestrator is a multi-file
bundle (index.html + styles.css + app.js + JSON data files) served as-is from the same
origin.
The deployed bundle is identical for every workspace. What each workspace contributes β the
list of pipelines installed there, with their UUIDs and parameters β lives in that workspace's own
file storage as a pipeline_cards.json, generated by the companion
create_pipeline_cards pipeline and read by the app at
page load. So changing the configuration means running a pipeline, not redeploying the webapp.
Agent-driven repo. This project is built with Claude Code. The authoritative, detailed instructions live in
CLAUDE.mdβ the agent reads it at session start. This README is the human-facing overview; when the two disagree,CLAUDE.mdwins.
/
βββ CLAUDE.md # Agent instructions (read by Claude Code at session start)
βββ README.md
βββ .gitignore
βββ .claude/ # Claude Code config (shared: settings + guardrail hook)
β βββ settings.json
β βββ hooks/block-destructive-git.js # Hard-blocks destructive git commands
β
βββ app/ # One generic bundle per UI variant β shared by ALL workspaces
β βββ flowchart/ # 2D node/edge flow-diagram map. English only
β β βββ index.html # Minimal shell
β β βββ styles.css # All styling
β β βββ app.js # All logic (renders map, merges cards, runs/polls pipelines)
β β βββ pipeline_map.json # Hand-authored map (layout + dependency edges), this variant only
β βββ cockpit/ # Guided one-step-at-a-time walkthrough. Bilingual EN|FR; embeds HTML reports
β β βββ index.html # Same 4-file shape as flowchart β variant owns its own copies
β β βββ styles.css
β β βββ app.js # Also holds the I18N string table (app chrome, EN + FR)
β β βββ pipeline_map.json # May drift from flowchart's map; `label`s are {en, fr}
β βββ pipeline_descriptions.json # Shared node descriptions β ONE copy, every variant AND workspace; values are {en, fr}
β
βββ utils_pipelines/ # OpenHEXA pipelines that support the webapp itself
β βββ create_pipeline_cards/ # Generates each workspace's pipeline_cards.json in its bucket
β βββ pipeline.py # The pipeline
β βββ config.py # GraphQL queries, type map, output paths
β βββ README.md # What it does, parameters, output location, caveats
β
βββ schemas/ # Machine-readable contracts / references
β βββ pipeline_map.schema.json # Schema for app/<variant>/pipeline_map.json
β βββ pipeline_cards.schema.json # Output contract for the generated pipeline_cards.json
β βββ pipeline_descriptions.schema.json # Schema + Markdown-lite rules for pipeline_descriptions.json
β βββ schema.generated.graphql # OpenHEXA GraphQL schema β query reference for agents
β
βββ docs/ # Consolidated knowledge (stable)
β βββ PRODUCT_SPEC.md # Product spec: functionality, UI variants, v0/v1/v2 roadmap
β βββ personas/ # UX persona / discovery questionnaires
β
βββ design/ # WIP / design explorations (not contracts)
β βββ wireframes/ # UX/visual targets for the orchestrator:
β β βββ orchestrator_wireframe.html # flowchart variant
β β βββ orchestrator_wireframe_cockpit.html # cockpit variant (the live target)
β β βββ orchestrator_wireframe_cockpit_narrative.html # narrative layer β PARKED (v2)
β β βββ orchestrator_frosted.html # visual-style exploration
β βββ grid_editor.html # Interactive map-layout editor
β βββ pipeline_map_preview.html # Standalone visual render of pipeline_map.json (for review)
β βββ pipeline_map_20260625.png # Map sketch (Whimsical)
β
βββ archive/ # Retired spikes & pre-orchestrator single-file apps (reference only)
βββ snt-app-dev/report-embed/ # Report-iframe feasibility spike (since built into cockpit)
βββ snt-testing/ # dhis2_reporting_rate, population_transformation(_split), status_spike
βββ snt-drc-workshop-demo/ # Older flat single-file webapp
(A local-only ignore/ folder is git-ignored personal scratch β not part of the project.)
Generic artifacts live in app/<variant>/ (one bundle per UI variant) plus schemas/ /
docs/ / design/. Cross-variant shared text lives in app/pipeline_descriptions.json β
one hand-authored copy of every node's description, deployed unchanged into both variants'
bundles. See "UI variants" below for why there's more than one app/ subfolder.
Nothing workspace-specific is committed here. There used to be a
workspaces/folder holding onepipeline_cards.jsonper workspace per variant; it was removed on 2026-08-04 when both variants started reading that catalog straight from the workspace's own file storage. Adding a new workspace now means deploying the same bundle and runningcreate_pipeline_cardsthere β no new repo file at all.
No stored webapp config either. Webapp identity and scopes (id, slug, URL, allowed operations) are resolved live from the OpenHEXA API (
list_static_webapps/get_static_webapp) at deploy time β the repo no longer keeps aworkspace_config.json.
The orchestrator can exist as more than one independently-deployable UI variant β same pipeline
data, different layout/UX. Each variant is a fully self-contained bundle under app/<variant>/
(including its own pipeline_map.json β variants do not share files, by design, so each UI can
evolve independently).
| Variant | Status | What it is |
|---|---|---|
flowchart |
Production β 3 workspaces | Interactive 2D node/edge map + config/run sidebar. English only; links out to HTML reports |
cockpit |
Production β 2 workspaces; v1 lead | Focused, one-step-at-a-time guided walkthrough. Bilingual EN|FR, and embeds HTML reports in-app. Target UX: design/wireframes/orchestrator_wireframe_cockpit.html |
New functionality generally lands in cockpit first (it's the v1 lead variant), so the two
variants are deliberately not feature-equal β see the feature notes above.
Deploying a given workspace + variant combination is 5 files β 4 generic (app/<variant>/*) +
1 cross-variant shared (app/pipeline_descriptions.json, same content in both variants' bundles).
There is no workspace-specific file: the bundle is byte-identical in every workspace, and the
per-workspace pipeline catalog is read at runtime from the workspace's own file storage.
Since webapp identity isn't stored in the repo, live webapps are told apart by slug β
snt-pipelines-orchestrator (flowchart) vs snt-pipelines-orchestrator-cockpit (cockpit).
Slugs are consistent everywhere; names are not β snt-app-dev and snt-testing use the
SNT Pipelines Orchestrator - Flowchart / - Cockpit convention, but cmr-snt-process is
still named the bare SNT Pipelines Orchestrator.
The orchestrator separates concerns across four kinds of file. The stable join key everywhere is
the node id == the pipeline's Python function name (e.g. snt_dhis2_extract).
| File | Scope | Holds |
|---|---|---|
app/<variant>/pipeline_map.json |
per-variant, workspace-independent | all nodes, labels, grid position, type, mutex group, directed edges (deps) |
app/pipeline_descriptions.json |
shared across every variant AND workspace | hand-authored, Markdown-lite description per node, keyed by id, as {en, fr} |
pipeline_cards.json β in the workspace's file storage, not this repo |
per-workspace, shared by both variants | which pipelines exist + uuid + parameters (drives active vs greyed) |
app/<variant>/index.html + app/<variant>/app.js + app/<variant>/styles.css |
per-variant app shell (multi-file) | renders the UI, merges map + descriptions with cards, runs/polls pipelines |
The map is identical across all workspaces for a given variant β every orchestrator shows
the same full diagram. What differs per workspace is only which nodes are active: a node is
available iff its id appears in that workspace's pipeline_cards.json. Pipelines not present
render greyed-out and unclickable. The map is hand-authored (validated against
schemas/pipeline_map.schema.json), not generated from the API.
pipeline_cards.json is the one piece of per-workspace data, and it is not in this repo. Each
workspace holds its own copy in its file storage at
utils_pipelines/create_pipeline_cards/pipeline_cards/pipeline_cards.json
written there by the create_pipeline_cards pipeline,
which lists the workspace's live pipelines, matches them against the deployed map, and reads each
one's current parameters. The webapp fetches it at page load through a signed URL (using the
FILES_READ scope it already has for report downloads).
Why it's set up this way:
- Config changes need no redeploy. Install a pipeline, or change a parameter, then re-run
create_pipeline_cardsβ the app picks it up on the next refresh. - Parameters match what's installed. They're read from each pipeline's deployed version in that
workspace, not scraped from GitHub
main, so they can't drift from the version actually there. - One catalog per workspace, shared by both variants β the two maps declare the same nodes, so there's no reason to generate it twice.
- It fails loudly if absent. A workspace where the generator has never run shows a clear
"run
create_pipeline_cards" message instead of a half-working app.β οΈ That means every workspace needs the generator installed and run once before its orchestrator works. Done insnt-app-devandsnt-testing; not yet incmr-snt-process, whose already-deployed flowchart app keeps running but would need it before the next redeploy. - Each run archives the previous catalog under
β¦/pipeline_cards/historical/with a timestamp, so you can see what changed.
For a given variant, the deployed bundle is 5 files: 4 generic + 1 cross-variant shared β and nothing workspace-specific. This is what makes the orchestrator portable: a new workspace reuses the whole bundle byte-for-byte and supplies its own catalog out-of-band, by running a pipeline.
| File | Generic / Shared | Notes |
|---|---|---|
index.html |
Generic (per variant) | Empty page shell β identical across workspaces for this variant. |
styles.css |
Generic (per variant) | All styling β no workspace details. |
app.js |
Generic (per variant) | All logic β zero hardcoded workspace specifics (see caveat below). |
pipeline_map.json |
Generic (per variant) | The SNT process map for this variant β same in every workspace, but NOT shared with other variants. |
pipeline_descriptions.json |
π Shared | Node description text (bilingual {en, fr}) β one repo copy under app/, deployed identically into both variants' bundles (same content, deployed twice). |
(not deployed) pipeline_cards.json |
Which pipelines exist here + their uuid + parameters. Generated by create_pipeline_cards, fetched by the app at load. Never put it in the bundle. |
Webapp identity and scopes (id, slug, URL, allowed operations) are not stored in the repo β
they're resolved live from the OpenHEXA API (list_static_webapps / get_static_webapp) at
deploy time, and are never fetched by the running app.
The app self-adapts at runtime: OpenHEXA injects window.OPENHEXA.workspaceSlug at page load,
so the same app.js queries this workspace, fetches this workspace's catalog, and greys out any
map node whose id isn't in it. β New workspace (same variant) = deploy the same 5 files, then
run create_pipeline_cards in that workspace.
One caveat to the "fully generic" claim:
app.jshardcodes the SaaS front-end basehttps://app.openhexa.org(for run / dataset links). That's the same for every SaaS workspace, but a self-hosted OpenHEXA install would need it changed β it's the only non-per-workspace assumption baked into the code.
French is the main interface language for v1 β nearly all users are French-speaking. It's built as one webapp with an EN/FR toggle in the header, not as separate per-language builds.
Shipped in the cockpit variant only. flowchart is still English-only. Both variants read
the same shared app/pipeline_descriptions.json, so its bilingual shape has to keep working for
both β flowchart simply reads the en side.
What is and isn't translated:
| Text | Where it lives | Translated? |
|---|---|---|
| App chrome (buttons, statuses, headings) | I18N table in app/cockpit/app.js |
β EN + FR |
| Pipeline / step titles | app/cockpit/pipeline_map.json β label |
β
{en, fr} |
| Pipeline descriptions | app/pipeline_descriptions.json |
β
{en, fr} |
| Parameter labels, help text, dropdowns | the workspace's pipeline_cards.json |
β English (comes from pipeline source) |
If you edit a pipeline description, write both languages. Values are
{ "en": "β¦", "fr": "β¦" } objects holding the same Markdown-lite prose. (A plain string still
works and is treated as English, so older entries don't break.)
The toggle remembers your choice (localStorage), and you can deep-link a language with
?lang=fr. Switching language re-renders the whole panel.
β οΈ The French strings are drafts pending review β they haven't been signed off as final copy.
Two independent tracks feed the running app β the bundle you deploy, and the catalog the workspace generates for itself:
IN THIS REPO β deployed, identical for every workspace
app/<variant>/pipeline_map.json β hand-authored map for this variant: layout + dependency edges
app/pipeline_descriptions.json β hand-authored node descriptions, shared across variants + workspaces
app/<variant>/index.html + styles.css + app.js β this variant's app shell
β
deploy set = app/<variant>/* + app/pipeline_descriptions.json
β (flattened β all 5 files land in the webapp root, side by side)
OpenHEXA static webapp (webapp id/slug resolved live via list_static_webapps; variant told
apart live by webapp SLUG β see "UI variants")
β
β fetched at page load via a signed URL (FILES_READ)
β
IN THE WORKSPACE β generated per workspace, never deployed
utils_pipelines/create_pipeline_cards/ (this repo) β deployed to the workspace as a pipeline
β run it (schemas/pipeline_cards.schema.json is the output contract)
<workspace files>/utils_pipelines/create_pipeline_cards/pipeline_cards/pipeline_cards.json
β this workspace's catalog: names, UUIDs, parameters
Open this directory in Claude Code. The agent reads CLAUDE.md for full instructions
automatically. For orchestrator work it also reads docs/PRODUCT_SPEC.md (product scope +
v0/v1/v2 roadmap) at session start. Then just describe what you want:
- Change the app itself: "Make the Run button in the cockpit do X" β the agent edits
app/<variant>/and redeploys the bundle - Set up a new workspace: "Create the flowchart webapp for workspace Z" β the agent creates the
webapp with the right scopes and deploys the standard 5-file bundle, then asks you to run
create_pipeline_cardsthere (that's what makes the pipelines appear) - A newly installed pipeline isn't showing up: run
create_pipeline_cardsin that workspace and refresh the app β no agent or redeploy needed at all
The agent needs access to the OpenHEXA MCP server (configured in Claude Code settings) to look up workspace/pipeline IDs and deploy webapps.
Guardrails are enforced, not just documented. CLAUDE.md opens with agent guardrails (ask
before any git/GitHub write; never do anything destructive or history-rewriting; hand
faster-by-hand steps back to you), and .claude/hooks/block-destructive-git.js is a PreToolUse
hook that hard-denies the destructive commands (git reset --hard, push --force, rebase,
clean, branch/tag deletion, stash drop, restore, filter-branch, gh pr close,
gh repo delete, gh api -X DELETE, β¦) regardless of what the agent is asked to do. If you want
one of those, run it by hand.
- Open Claude Code in this directory
- Say: "Create the
<variant>webapp for workspace<workspace name>" (specify which UI variant βflowchartorcockpit) - The agent will:
- Find the workspace slug via
list_workspaces - Create the webapp with the four required scopes (
PIPELINES_READ,PIPELINES_RUN,FILES_READ,USER_READ) βFILES_READis what lets the app read its own catalog - Deploy the generic
app/<variant>/bundle +app/pipeline_descriptions.json(5 files) - Check the catalog exists in the workspace's file storage
- Find the workspace slug via
- Install
create_pipeline_cardsin the workspace (fromutils_pipelines/create_pipeline_cards/) and run it once. It takes no parameters β just Run. - Nothing to commit β the bundle is unchanged and the catalog isn't a repo file.
The webapp comes first.
create_pipeline_cardscurates the catalog against the deployedpipeline_map.jsonof the webapp named by itsconfig.WEBAPP_SLUGβ currently the Cockpit app,snt-pipelines-orchestrator-cockpit. Until that webapp exists in the workspace, the generator run fails. So if you are only adding theflowchartvariant to a workspace, either deploy Cockpit there too, or pointWEBAPP_SLUGatsnt-pipelines-orchestratorand redeploy the generator. In the other direction, the app itself shows a "runcreate_pipeline_cards" error until step 4 is done β so both halves are needed before the orchestrator works.
There is nothing to add to this repo, and nothing to redeploy:
- Install the pipeline in the workspace as usual (from its template)
- Run
create_pipeline_cardsin that workspace - Refresh the orchestrator β the pipeline's node is now active, with its parameter form generated from the version you just installed
The only case that still needs a repo change is a pipeline that isn't on the map yet: the map
(app/<variant>/pipeline_map.json) is hand-authored, so a genuinely new step needs a node adding
there β and that does mean a redeploy. Pipelines already on the map but not installed simply render
greyed-out until you install them.
The catalog carries a
generated_atdate, but its parameters come from each pipeline's deployed version in that workspace β not from GitHubmainβ so they can't drift from what's actually installed. What can be out of date is the catalog vs the workspace: if pipelines have been installed or upgraded since that date, re-run the generator.Each entry also records the deployed version it was built from (
version_name/version_number), so the app can eventually notice that drift by itself rather than relying on someone spotting it.
Deploys go through the OpenHEXA MCP tools (the webapp id/slug is resolved live via
list_static_webapps). There are two paths, and the agent picks by the size of the change:
- Small, targeted edits (a few lines of
app.js, a CSS tweak, one description) go throughedit_static_webapp_fileβ a find/replace applied server-side, so the agent never loads the whole file. This is the normal path. - New files, wholesale rewrites, or a first deploy go through
update_static_webapp, which takes file contents inline.
Partial deploys work β only the changed files need to be sent β and after every deploy the agent
re-reads the changed file(s) live (get_static_webapp_file for one file, get_static_webapp for
the whole bundle) and diffs them against the repo (app/<variant>/ +
app/pipeline_descriptions.json) to confirm what's live matches the source.
Known friction (large files). The update_static_webapp path only accepts file contents,
not a file path, and the agent can only load a file into its context up to a size limit. Each
app.js is now ~90 KB β well past that β so a wholesale rewrite of one means reading it back
in slices and reassembling it. Targeted edits avoid this entirely (see above), so this only bites
on a full rewrite. It's verified each time (the live file is diffed against the local copy), so
it's a speed bump, not a risk.
Manual fallback β drag-drop from the repo. Because the bottleneck is only about getting the
bytes into the agent, uploading a file yourself through the browser avoids it entirely. If the
OpenHEXA UI lets you replace files on an existing webapp (Web Apps β the webapp β
edit/settings), you can drag the changed file(s) straight from app/<variant>/ (generic to
that variant) or app/pipeline_descriptions.json into the UI β the repo copy is
always the up-to-date source, so this is safe. (The OpenHEXA
CLI can deploy pipelines from local files but not static webapps today, so there's no
command-line shortcut yet β a request to add one has been raised with the OpenHEXA team.)
If schemas/schema.generated.graphql becomes stale, regenerate it with:
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/BLSQ/openhexa-app/main/frontend/schema.generated.graphql" -OutFile "schemas/schema.generated.graphql"The active product is the SNT Pipelines Orchestrator, deployed per variant as that variant's
generic app/<variant>/ bundle (plus the shared descriptions) β the same 5 files everywhere, with
each workspace's catalog supplied from its own file storage:
Each (workspace, variant) pair is its own webapp with its own URL β five in total (verified live 2026-07-31):
| Workspace | Slug | Variant | URL |
|---|---|---|---|
| SNT App Dev | snt-app-dev |
flowchart |
https://snt-pipelines-orchestrator.openhexa.io/ |
| SNT App Dev | snt-app-dev |
cockpit |
https://snt-pipelines-orchestrator-cockpit.openhexa.io/ |
| SNT Testing | snt-testing |
flowchart |
https://snt-testing-snt-pipelines-orchestrator.openhexa.io/ |
| SNT Testing | snt-testing |
cockpit |
https://snt-testing-snt-pipelines-orchestrator-cockpit.openhexa.io/ |
| CMR SNT Process | cmr-snt-process |
flowchart |
https://cmr-snt-process-snt-pipelines-orchestrator.openhexa.io/ |
cockpit is not deployed to cmr-snt-process. Note cmr-snt-process's webapp is still named the
bare SNT Pipelines Orchestrator, while the other two workspaces use the
β¦ - Flowchart / β¦ - Cockpit naming β match on slug, not name (see "UI variants").
URLs and IDs are resolved live from the OpenHEXA API (list_static_webapps) β that, not this
table, is the source of truth.
Legacy single-pipeline webapps and spikes (A.2 DHIS2 Formatting in the DRC workshop demo; DHIS2 Reporting Rate, Population Transformation, and the status spike in SNT Testing; the report-embed feasibility probe in SNT App Dev) were the stepping stones toward the orchestrator. Their local copies now live under
archive/and are no longer maintained. A few of the spike webapps are still present on the platform (e.g.t0-9-status-proxy-spike,report-embed-probe) β they're leftovers, not part of the product.
The SNT Pipelines Orchestrator is built in the dedicated
snt-app-devworkspace (all ~18 pipelines installed β the primary build target) and also deployed tosnt-testing(a subset installed, so it demos the greyed-out state). Theflowchartvariant additionally reachescmr-snt-process;cockpitis live insnt-app-devandsnt-testingonly.