Developing Against the GA4GH WES API
This is a developer-facing companion to the admin GA4GH docs. The admin docs cover configuring and verifying the service; this guide walks an API client through actually submitting and monitoring Galaxy workflow runs over the GA4GH Workflow Execution Service (WES) v1.0.0 wire protocol.
Every request below targets a Galaxy server and uses Galaxy’s workflow formats. The curl examples were exercised against a live development server; the responses shown are real (ids shortened/anonymized).
Example clients
If you would rather drive the flow below from a runnable tool than from raw curl, two small
reference clients walk exactly this sequence (discover → stage → submit → poll → outputs →
tasks). Both expose a subcommand-per-endpoint CLI plus a demo subcommand that runs the
whole example end to end, and both are published to PyPI so they run straight under
uv without installing anything:
export GXY_WES_API_KEY=...
uvx gxy-wes demo --galaxy-url http://localhost:8080 # requests-only
uvx gxy-wes-bioblend demo --galaxy-url http://localhost:8080 # BioBlend-backed
gxy-wes — a hand-rolled
requestswrapper with no dependency beyondrequests, so it stays dependency-light and reads as a literal transcript of the wire protocol.gxy-wes-bioblend — the same CLI surface, but every call is routed through a BioBlend
GalaxyInstance, showing the shape a BioBlend-based application would use.
Important
These two packages are illustrative examples, not production-supported projects — they exist to make this document executable and to show the API two different ways. Do not build a product on them. For real workflow-execution tooling use Planemo or talk to the Galaxy API through BioBlend directly.
How Galaxy maps onto WES
WES is workflow-engine agnostic. Galaxy implements it on top of its existing workflow invocation machinery, so the WES nouns map onto Galaxy nouns:
WES concept |
Galaxy concept |
|---|---|
|
|
|
An invocation step, or a single job within a collection-mapping step |
run |
Invocation outputs (HDAs / HDCAs), decorated with DRS URIs |
|
A Galaxy workflow format (see below) — not CWL/Nextflow/WDL |
workflow_type is one of Galaxy’s own two workflow formats:
gx_workflow_format2— the gxformat2 (“Format2”) YAML format. CWL-like, hand-authorable.gx_workflow_ga— the native.gaJSON format produced by Galaxy’s workflow editor.
workflow_type_version is currently accepted as a free-form string (e.g. "1.0.0"); the
type itself is what matters, and Galaxy will also auto-detect it from the workflow body
(class: GalaxyWorkflow ⇒ format2; a top-level steps/workflow key ⇒ .ga).
Base URL and authentication
All endpoints live under /ga4gh/wes/v1/. With a local dev server that is:
http://localhost:8080/ga4gh/wes/v1/
Every endpoint except service-info requires authentication. Galaxy uses its normal API
key auth — pass x-api-key: <key> (the WES spec’s bearer-token scheme is not used). Get a
key for an existing account with HTTP basic auth:
curl -s -u "you@example.com:password" \
http://localhost:8080/api/authenticate/baseauth
# {"api_key": "317ab17d..."}
Anonymous and inactive accounts are rejected on submission and on every run-scoped read.
1. Discover the service
service-info is public and advertises the supported formats, engine parameters, and
filesystem protocols.
curl -s http://localhost:8080/ga4gh/wes/v1/service-info | jq .
Trimmed to the WES-relevant fields (the real response also carries
system_state_counts, auth_instructions_url, tags, and the standard GA4GH
organization/version/environment keys):
{
"id": "localhost.wes",
"name": "Galaxy WES API",
"type": { "group": "org.ga4gh", "artifact": "wes", "version": "1.0.0" },
"workflow_type_versions": {
"gx_workflow_ga": { "workflow_type_version": ["1.0.0"] },
"gx_workflow_format2": { "workflow_type_version": ["1.0.0"] }
},
"supported_wes_versions": ["1.0.0"],
"supported_filesystem_protocols": ["http", "https", "file", "s3", "gs"],
"workflow_engine_versions": {
"galaxy": { "workflow_engine_version": ["1.0.0"] }
},
"default_workflow_engine_parameters": [
{ "name": "history_name", "type": "string", "default_value": "" },
{ "name": "history_id", "type": "string", "default_value": "" },
{
"name": "preferred_object_store_id",
"type": "string",
"default_value": ""
},
{ "name": "use_cached_job", "type": "boolean", "default_value": "false" }
]
}
2. Provide the workflow and its inputs
A WES RunRequest is multipart/form-data. The fields Galaxy honors:
Field |
Required |
Notes |
|---|---|---|
|
yes |
|
|
yes |
Free-form string, e.g. |
|
one of url/attachment |
A URL Galaxy can fetch ( |
|
one of url/attachment |
The workflow file uploaded inline. |
|
no |
JSON object of workflow inputs (see below). |
|
no |
JSON object of Galaxy-specific run options (see below). |
|
no |
Accepted but currently not persisted onto the invocation. |
|
no |
Accepted; informational. |
workflow_params — wiring up inputs
Galaxy workflow inputs are keyed by the workflow’s input label, and dataset inputs are
references to data that already exists in Galaxy. Galaxy invokes with inputs_by="name",
so the keys are the workflow input names.
A dataset input is a {"src": ..., "id": ...} reference:
{ "input1": { "src": "hda", "id": "1e8ab44153008be8" } }
src is hda for a single dataset or hdca for a dataset collection; id is the encoded
content id. Simple parameter inputs (integers, text, booleans) are passed as plain JSON
values under their input name.
WES has no data-staging endpoint of its own. You stage inputs through Galaxy’s normal API first (create a history, upload/fetch datasets) and then reference them here.
workflow_engine_parameters — Galaxy run options
A JSON object. Recognized keys (also advertised in service-info):
Key |
Effect |
|---|---|
|
Run into this existing history (encoded id). Otherwise a new history is created. |
|
Name for the auto-created history (default |
|
Object store for the run’s outputs. |
|
|
Referencing a workflow already in Galaxy: gxworkflow://
Galaxy adds a non-standard URL scheme so you don’t have to ship the workflow body on every
submission. Use it as workflow_url:
gxworkflow://<encoded_workflow_id> # the StoredWorkflow (latest version)
gxworkflow://<encoded_workflow_id>?instance=true # a specific Workflow instance
The workflow only has to be accessible to the caller — the same rule a normal invocation uses: owned by them, shared with them, published/importable, or the caller is an admin. Anything else is a 403.
With gxworkflow://, Galaxy skips the import step and invokes the referenced workflow
directly. workflow_type is still required by the form but is not validated against
the stored workflow in this case — the “must match the auto-detected type or 400” check
only applies to inline workflow_attachment / fetched workflow_url submissions.
3. Submit the run
Staging step (Galaxy-native, abbreviated) — create a history and upload one dataset:
KEY=317ab17d...; BASE=http://localhost:8080
HIST=$(curl -s -X POST "$BASE/api/histories" -H "x-api-key: $KEY" \
-H 'Content-Type: application/json' -d '{"name":"WES Tutorial"}' | jq -r .id)
HDA=$(curl -s -X POST "$BASE/api/tools/fetch" -H "x-api-key: $KEY" \
-F "history_id=$HIST" \
-F 'targets=[{"destination":{"type":"hdas"},"elements":[{"src":"pasted","paste_content":"line one\nline two\n","ext":"txt","name":"wes_input"}]}]' \
| jq -r '.outputs[0].id')
A minimal Format2 workflow (simple.gxwf.yml):
class: GalaxyWorkflow
name: Simple Workflow
inputs:
input1: data
outputs:
wf_output_1:
outputSource: first_cat/out_file1
steps:
first_cat:
tool_id: cat1
in:
input1: input1
Submit it (the WES POST /runs):
curl -s -X POST "$BASE/ga4gh/wes/v1/runs" -H "x-api-key: $KEY" \
-F "workflow_type=gx_workflow_format2" \
-F "workflow_type_version=1.0.0" \
-F "workflow_params={\"input1\":{\"src\":\"hda\",\"id\":\"$HDA\"}}" \
-F "workflow_engine_parameters={\"history_id\":\"$HIST\"}" \
-F "workflow_attachment=@simple.gxwf.yml;type=application/x-yaml"
{ "run_id": "33b43b4e7093c91f" }
The run_id is the encoded Galaxy invocation id. Batch invocations (multiple invocations
from one request) are rejected by WES.
4. Monitor the run
Abbreviated status:
curl -s "$BASE/ga4gh/wes/v1/runs/33b43b4e7093c91f/status" -H "x-api-key: $KEY"
# {"run_id":"33b43b4e7093c91f","state":"RUNNING"}
Full run log — outputs are decorated with DRS URIs so a client can hand them straight to the DRS API:
curl -s "$BASE/ga4gh/wes/v1/runs/33b43b4e7093c91f" -H "x-api-key: $KEY" | jq .
{
"run_id": "33b43b4e7093c91f",
"request": null,
"state": "COMPLETE",
"run_log": null,
"task_logs_url": "/ga4gh/wes/v1/runs/33b43b4e7093c91f/tasks",
"task_logs": null,
"outputs": {
"wf_output_1": {
"src": "hda",
"id": "417e33144b294c21",
"workflow_step_id": 30,
"drs_uri": "drs://drs.localhost:8080/hda-4dcf52aeee371f21"
}
}
}
Notes on the run log:
requestis alwaysnull— Galaxy does not persist the originalRunRequest, so it cannot be reconstructed.task_logsis intentionallynull(deprecated in the WES spec). Usetask_logs_url.Only HDA (single-dataset) outputs get a
drs_uri; collection (HDCA) outputs carry an encodedidbut no DRS URI.
State mapping
WES has a single flat State enum (QUEUED, INITIALIZING, RUNNING, COMPLETE,
EXECUTOR_ERROR, CANCELING, CANCELED, …). A Galaxy run is a WorkflowInvocation,
whose state is mapped onto that enum (GALAXY_TO_WES_STATE in services/wes.py):
Galaxy invocation state |
WES state |
Meaning |
|---|---|---|
|
|
Invocation created, not yet scheduling. |
|
|
Deferred inputs are being materialized. |
|
|
Ready for a scheduling iteration. |
|
|
Fully scheduled; jobs are running/queued. |
|
|
Scheduling done and every job reached a terminal state. |
|
|
The invocation itself failed to schedule. |
|
|
Invocation was cancelled. |
|
|
Cancellation requested, in progress. |
(anything unmapped) |
|
Fallback; should not occur for current states. |
Poll status (or the run log state) until the state is terminal: COMPLETE (success),
EXECUTOR_ERROR (failure), or CANCELED (cancelled). Keep polling while it is any of the
non-terminal states (QUEUED, INITIALIZING, RUNNING, CANCELING).
Why completed ⇒ COMPLETE, even when a job failed
This is intentional and important: a failed job does not make the run unsuccessful.
WES state describes the run (the invocation), not individual jobs. Galaxy’s completed
means scheduling finished and all jobs reached a terminal state (ok, error, skipped,
paused, …) — and a job ending in error is frequently part of a perfectly normal,
valid execution path. For example, a workflow can route a step through a filter-failed
collection operation precisely so that some jobs are allowed to fail and the failures are
filtered out downstream; the invocation still completes successfully.
So:
The only invocation state that maps to
EXECUTOR_ERRORis Galaxy’s ownfailed— i.e. the invocation could not be scheduled — not “some job errored.”Clients should not infer run failure from a non-zero per-task
exit_code. Per-taskexit_codes (from the tasks endpoint) are for inspecting individual steps, not for deciding whether the run as a whole succeeded.Determine run success from the run
state(COMPLETE); inspect outputs and per-task detail for finer-grained results.
5. Tasks (per-step / per-job detail)
task_logs_url points at a paginated task list. A task_id is the step order_index
(e.g. "0", "1"), or order_index.job_index (e.g. "1.0", "1.2") for the individual
jobs of a collection-mapping step.
curl -s "$BASE/ga4gh/wes/v1/runs/33b43b4e7093c91f/tasks" -H "x-api-key: $KEY" | jq .
{
"task_logs": [
{
"id": "0",
"name": "input1",
"exit_code": null,
"stdout": null,
"stderr": null
},
{
"id": "1",
"name": "first_cat",
"exit_code": 0,
"stdout": "/api/jobs/417e33144b294c21/stdout",
"stderr": "/api/jobs/417e33144b294c21/stderr"
}
],
"next_page_token": null
}
stdout / stderr are URLs to plain-text job-output endpoints
(GET /api/jobs/{job_id}/stdout and /stderr). Steps with no underlying job — input
steps, for example — have null exit_code, stdout, and stderr (as id: "0" shows
above). Fetch a single task with GET /ga4gh/wes/v1/runs/{run_id}/tasks/{task_id}.
Pagination
Both GET /runs and GET /runs/{id}/tasks use opaque keyset (cursor) tokens, not
offsets. Pass page_size (1–100, default 10); if more results exist the response includes
next_page_token, which you echo back as page_token on the next request. Do not try to
decode or construct tokens yourself.
curl -s "$BASE/ga4gh/wes/v1/runs?page_size=2" -H "x-api-key: $KEY"
curl -s "$BASE/ga4gh/wes/v1/runs?page_size=2&page_token=<next_page_token>" -H "x-api-key: $KEY"
GET /runs only ever lists the authenticated user’s own runs.
6. Cancel a run
curl -s -X POST "$BASE/ga4gh/wes/v1/runs/33b43b4e7093c91f/cancel" -H "x-api-key: $KEY"
# {"run_id": "33b43b4e7093c91f"}
This requests cancellation of the underlying invocation; the run then transitions through
CANCELING to CANCELED.
Errors
Errors come back as Galaxy’s standard JSON error body ({"err_msg": ..., "err_code": ...})
with a matching HTTP status. The ones a client is most likely to hit:
Situation |
Status |
|---|---|
Missing/invalid required form field (e.g. no |
400 |
|
400 |
Malformed |
400 |
Submit while anonymous |
403 |
Submit while logged in but inactive (unactivated account) |
403 |
Run / task id does not exist |
404 |
Invalid |
404 |
Access to resources you do not own is not reported uniformly — watch for this:
Reading a run you don’t own →
403(AuthenticationRequired).Submitting against a history you don’t own →
404(ObjectNotFound).A
gxworkflow://reference to a workflow you cannot access (not owned, not shared with you, not published) →403(ItemAccessibilityException).
So a 404 on submission can mean “your history id is wrong” or “that history belongs to
someone else”; don’t assume 404 always means the object is absent.
Endpoint summary
Method |
Path |
Auth |
Purpose |
|---|---|---|---|
GET |
|
public |
Capabilities and supported formats |
POST |
|
required |
Submit a run ( |
GET |
|
required |
List the user’s runs (keyset paginated) |
GET |
|
required |
Full run log + DRS-decorated outputs |
GET |
|
required |
Abbreviated |
POST |
|
required |
Request cancellation |
GET |
|
required |
Paginated task list |
GET |
|
required |
Single task detail |
Known limitations and gotchas
Run
COMPLETEreflects the invocation, not individual jobs — by design, a failed job does not make the run fail (a failed job can be a normal, valid path, e.g. filter-failed). Only an invocation that fails to schedule maps toEXECUTOR_ERROR. Do not infer run failure from a per-taskexit_code. See §4.Inputs must be pre-staged — there is no WES upload endpoint; stage datasets via the Galaxy API and reference them by
{src, id}inworkflow_params.requestis never returned in the run log — the originalRunRequestis not stored.DRS URIs only on HDA outputs, not on collection outputs.
tagsare not persisted to the invocation, and run summaries returntags: {}.service-infoauth_instructions_urlis a placeholder ("TODO").The generated Pydantic models under
lib/galaxy/schema/wes/are produced bygen.shfrom the upstream GA4GH OpenAPI spec with no Galaxy post-processing — treat them as regenerable and avoid hand edits.
Source pointers
Router:
lib/galaxy/webapps/galaxy/api/wes.pyService:
lib/galaxy/webapps/galaxy/services/wes.pyGenerated models:
lib/galaxy/schema/wes/(regenerate viagen.sh)Shared GA4GH service-info builder:
lib/galaxy/webapps/galaxy/services/ga4gh.pyAPI tests (worked examples for every endpoint):
lib/galaxy_test/api/test_wes.py