Last updated 2026-05-07

Runner REST API

Every endpoint exposed by the runner. Defaults to http://localhost:4800; override the port with PORT=.... CORS is enabled, so the dashboard and any other origin can call directly. Routes are mounted under /api; the only exception is /health.

Workflow runs

POST /api/runs — Trigger a run

{
"workflow": "ci", // required: filename without ext, full filename, or workflow name
"inputs": { "skip-tests": "false" },
"sandbox": {
"strategy": "worktree", // worktree | copy | none. Default: auto.
"ref": "feature-branch" // optional. Default: HEAD.
}
}

Response: 201 Created with the new WorkflowRun.

curl -X POST http://localhost:4800/api/runs \
-H 'Content-Type: application/json' \
-d '{"workflow": "ci", "inputs": {"skip-tests": "false"}}'
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"workflow": "ci",
"status": "queued",
"jobs": [],
"createdAt": "2026-03-22T10:00:00.000Z",
"updatedAt": "2026-03-22T10:00:00.000Z",
"trigger": {
"event": "workflow_dispatch",
"ref": "refs/heads/main",
"sha": "abc123def456"
},
"inputs": { "skip-tests": "false" }
}

GET /api/runs — List runs

Query: ?status=queued|in_progress|completed|failed|cancelled — optional filter. Sorted newest first.

curl http://localhost:4800/api/runs
curl 'http://localhost:4800/api/runs?status=completed'

GET /api/runs/:id — Run detail

Full WorkflowRun with jobs and steps. 404 when not found.

GET /api/runs/:id/logs — Concatenated logs

Plain-text response with one section per job and step.

=== Job: build (build) ===
--- Step 1: Checkout [success] ---
Workspace already available, no checkout needed
--- Step 2: Install dependencies [success] ---
npm ci
...

GET /api/runs/:id/artifacts — List artifacts

[
{
"id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"name": "coverage-report",
"path": "/path/to/.artifacts/a1b2c3d4/coverage-report.html",
"size": 42567,
"mimeType": "text/html",
"runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
]

GET /api/runs/:id/artifacts/:name — Download an artifact

Streams the file with Content-Disposition: attachment.

curl -o report.html http://localhost:4800/api/runs/a1b2c3d4/artifacts/coverage-report

POST /api/runs/:id/cancel — Cancel a run

Sends an abort signal. Works on queued or in_progress; returns 409 otherwise.

Workflows

GET /api/workflows

Discovers every .yml / .yaml under .github/workflows/ and returns parsed metadata.

[
{
"name": "CI",
"fileName": "ci.yml",
"filePath": "/path/to/.github/workflows/ci.yml",
"triggers": ["push", "pull_request", "workflow_dispatch"],
"jobs": [
{ "id": "test", "name": "Run Tests", "needs": [] },
{ "id": "deploy", "name": "Deploy", "needs": ["test"] }
],
"inputs": {
"environment": {
"description": "Target environment",
"required": false,
"default": "staging",
"type": "choice",
"options": ["staging", "production"]
}
}
}
]

GET /api/workflows/:name

{
"info": {
"name": "CI",
"fileName": "ci.yml",
"filePath": "/path/to/.github/workflows/ci.yml",
"triggers": ["push", "workflow_dispatch"],
"jobs": [...],
"inputs": {...}
},
"definition": { /* raw parsed YAML */ }
}

Secrets

GET /api/secrets

Returns secret names only for the requested environment (?env=production). Values are never exposed.

["GITHUB_TOKEN", "NPM_TOKEN", "AWS_ACCESS_KEY_ID"]

PUT /api/secrets/:name

curl -X PUT http://localhost:4800/api/secrets/NPM_TOKEN \
-H 'Content-Type: application/json' \
-d '{"value": "npm_abc123", "environment": "production", "backend": "local"}'

DELETE /api/secrets/:name

curl -X DELETE http://localhost:4800/api/secrets/NPM_TOKEN

POST /api/secrets/import

.env-formatted content: KEY=VALUE lines, # comments, blank lines, optional quoted values. Set type to variable to import into the variables store instead of the secrets store.

curl -X POST http://localhost:4800/api/secrets/import \
-H 'Content-Type: application/json' \
-d '{"content": "NPM_TOKEN=abc\n# comment\nAWS_KEY=xyz", "environment": "production"}'

POST /api/secrets/sync

Pulls from active backends into the local runner mirror. For repo-backed SOPS + age, this decrypts the managed file from .stax/vault.json.

GET /api/secrets/status

Returns backend availability and mirror status.

GET /api/variables, PUT /api/variables/:name, DELETE /api/variables/:name

Variables mirror GitHub Actions vars.*. They are environment scoped like secrets but are not masked.

GET /api/audit/secrets

Returns the local secrets audit log.

GitHub sync & deployments

GET /api/sync/config / PUT /api/sync/config

Reads or writes GitHub sync configuration, webhook HMAC settings, and deployment-log settings.

{
"enabled": true,
"github": {
"owner": "elloloop",
"repo": "stax",
"syncIssues": true,
"syncDiscussions": false,
"syncWiki": false,
"syncReleases": false,
"pollIntervalMs": 30000,
"webhookSecret": "optional"
},
"deploymentLog": {
"enabled": true,
"issueNumber": 42,
"environments": ["production", "prod"]
}
}

POST /api/sync/github

Runs a pull sync for enabled GitHub issues, discussions, wiki pages, and releases.

POST /api/sync/pr

Pushes a local stacked diff to GitHub as a PR branch.

POST /api/sync/inbox

Receives signed GitHub webhook events. Uses STAX_GH_WEBHOOK_SECRET or the configured webhook secret to verify x-hub-signature-256.

GET /api/sync/issues, /discussions, /wiki, /releases, /audit

Returns the local GitHub sync caches and sync audit log.

GET /api/deployments

Returns local deployment ledger records from .stax/deployments.jsonl, newest first. Records include run ID, workflow, status, environment, ref, SHA, job summary, and optional GitHub issue mirror metadata.

Events & health

GET /api/events

Server-Sent Events stream. See SSE Events.

GET /health

{ "status": "ok", "workspace": "/path/to/your/project" }

Type definitions

type RunStatus = 'queued' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
type Conclusion = 'success' | 'failure' | 'cancelled' | 'skipped';
interface WorkflowRun {
id: string;
workflow: string;
status: RunStatus;
conclusion?: Conclusion;
jobs: Job[];
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
trigger: { event: string; ref: string; sha: string };
inputs?: Record<string, string>;
env?: Record<string, string>;
sandbox?: { path: string; strategy: string };
}
interface Job {
id: string;
name: string;
status: 'queued' | 'in_progress' | 'completed';
conclusion?: Conclusion;
steps: Step[];
startedAt?: string;
completedAt?: string;
outputs: Record<string, string>;
}
interface Step {
name: string;
status: 'queued' | 'in_progress' | 'completed';
conclusion?: Conclusion;
log: string;
outputs: Record<string, string>;
number: number;
}
interface Artifact {
id: string;
name: string;
path: string;
size: number;
mimeType: string;
runId: string;
}
interface WorkflowInfo {
name: string;
fileName: string;
filePath: string;
triggers: string[];
jobs: { id: string; name: string; needs: string[] }[];
inputs?: Record<string, WorkflowInput>;
}
interface WorkflowInput {
description?: string;
required?: boolean;
default?: string;
type?: string;
options?: string[];
}