Station
Station is a monitoring dashboard for Station. It connects to your signal and broadcast adapters and provides a web interface for inspecting registered signals, browsing run history, and watching broadcast DAG execution in real time.
Station is a combined:
- Hono API server — REST endpoints for signals, runs, broadcasts, and health checks, plus a WebSocket endpoint for real-time event streaming
- Next.js frontend — Dashboard UI that renders signal metadata, run history, broadcast DAG visualization, and live log output
The API server runs on the configured port (default 4400). The Next.js frontend runs on port + 1 (default 4401). Both start automatically when you launch Station.
Install
pnpm add station-kitConfiguration
Create a station.config.ts (or .js / .mjs) in your project root:
import { defineConfig } from "station-kit";import { SqliteAdapter } from "station-adapter-sqlite";import { BroadcastSqliteAdapter } from "station-adapter-sqlite/broadcast"; export default defineConfig({ port: 4400, signalsDir: "./signals", broadcastsDir: "./broadcasts", adapter: new SqliteAdapter({ dbPath: "./jobs.db" }), broadcastAdapter: new BroadcastSqliteAdapter({ dbPath: "./jobs.db" }),});The defineConfig helper provides type checking and autocompletion. It is a pass-through function — it returns the object unchanged.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
port | number | 4400 | HTTP port for the API server. The Next.js UI runs on port + 1. |
host | string | "localhost" | Hostname to bind both servers to. Set to "0.0.0.0" to listen on all interfaces. |
signalsDir | string | — | Directory containing signal definition files. Station imports these to display signal metadata: input/output schemas, timeouts, retry counts, intervals, and concurrency settings. Falls back to a signals/ directory in the working directory if one exists. |
broadcastsDir | string | — | Directory containing broadcast definition files. Station imports these to display DAG structure, failure policies, and node dependencies. Falls back to a broadcasts/ directory in the working directory if one exists. |
adapter | SignalQueueAdapter | MemoryAdapter | Signal storage adapter. Must point to the same database as your runner to see its data. See Adapters. |
broadcastAdapter | BroadcastQueueAdapter | — | Broadcast storage adapter. Required for broadcast monitoring features. If omitted and broadcastsDir is set, a memory adapter is used. |
runRunners | boolean | true | When true, Station runs its own SignalRunner and BroadcastRunner internally. Set to false for read-only monitoring of an existing runner’s database. |
open | boolean | true | Automatically open the dashboard in the default browser on startup. |
logLevel | "debug" | "info" | "warn" | "error" | "info" | Controls Station’s own console output verbosity. |
auth | { username, password, sessionTtlMs? } | — | Dashboard login credentials. When set, the dashboard presents a login screen. sessionTtlMs controls session expiry (default: 86,400,000 ms / 24 hours). Omit to disable auth. |
runner | Partial<RunnerConfig> | — | Override signal runner settings. Only applies when runRunners: true. Fields: |
Runner config (nested under runner)
| Option | Type | Default | Description |
|---|---|---|---|
pollIntervalMs | number | 1000 | Milliseconds between poll ticks for due runs. |
maxConcurrent | number | 5 | Maximum number of signal runs executing simultaneously. |
maxAttempts | number | 1 | Default maximum retry attempts for signals that do not specify their own. |
retryBackoffMs | number | 1000 | Base delay in milliseconds between retry attempts. |
Broadcast runner config (nested under broadcastRunner)
| Option | Type | Default | Description |
|---|---|---|---|
pollIntervalMs | number | 1000 | Milliseconds between poll ticks for due broadcast runs. |
Running Station
npx stationStation looks for station.config.ts (or .js / .mjs) in the current working directory. If no config file is found, it starts with default settings (MemoryAdapter, no signal directory).
Active mode vs. read-only mode
Station operates in one of two modes depending on the runRunners setting.
Active mode (runRunners: true, default)
Station creates its own SignalRunner and BroadcastRunner. It discovers signals from signalsDir, polls the adapter for due runs, and executes them. Use this when you want Station to be your only runner process. The dashboard provides full functionality: monitoring, triggering signals, and cancelling runs.
Read-only mode (runRunners: false)
Station only reads from the adapter. It does not create runners, does not execute signals, and does not poll for due runs. Use this when you have a separate runner process and want Station purely for monitoring. Trigger and cancel endpoints return 403 in this mode.
signalsDir and broadcastsDir to import signal/broadcast definitions for metadata display (schemas, intervals, DAG structure). Without these directories, the dashboard shows run data but not signal configuration details.Dashboard features
Signals list
View all registered signals with their name, input/output schemas (rendered from Zod definitions), timeout, retry count, recurring interval, concurrency settings, step names, and source file path.
Scheduled signals
Recurring signals get a dedicated view showing the interval, next scheduled run time, last execution time, and last execution status.
Run history
Browse all past and current signal runs. Filter by status (pending, running, completed, failed, cancelled) or by signal name. Each run shows the full detail: input data, output data, error messages, timing (created, started, completed), attempt count, and step execution records.
Run logs
View stdout and stderr output captured from signal handler execution. Logs are stored in an in-memory buffer and persisted to a separate SQLite database (station-logs.db) for survival across restarts.
Broadcast visualization
See the DAG structure of registered broadcasts — which nodes exist, their signal mappings, and dependency edges. During execution, node statuses (pending, running, completed, failed, skipped) update in real time. Includes skip reasons when nodes are bypassed due to guard conditions, upstream failures, or cancellation.
Real-time updates
A WebSocket connection on /api/events pushes lifecycle events as they happen. The frontend subscribes automatically — no polling required. Events cover the full signal and broadcast lifecycle (see WebSocket events table below).
Actions
In active mode, the dashboard allows triggering signals with custom input and cancelling in-progress runs or broadcast runs.
API endpoints
Station exposes a REST API on the configured port. All responses use the shape { data: ... } on success or { error: string, message: string } on failure.
Health
| Endpoint | Method | Description |
|---|---|---|
/api/health | GET | Health check. Calls ping() on the signal adapter and broadcast adapter (if configured). Returns { ok, signal, broadcast }. |
Signals
| Endpoint | Method | Description |
|---|---|---|
/api/signals | GET | List all registered signals with metadata — name, file path, input/output schemas, interval, timeout, max attempts, max concurrency, step names. |
/api/signals/scheduled | GET | List recurring signals with their interval, next scheduled run, last run time, and last run status. |
/api/signals/:name | GET | Get details for a specific signal. Returns 404 if not found. |
/api/signals/:name/trigger | POST | Trigger a signal with optional input. Body: { "input": { ... } }. Returns the new run ID. Returns 403 in read-only mode, 404 if signal not found. |
/api/signals/:name/runs | GET | List all runs for a specific signal. |
Runs
| Endpoint | Method | Description |
|---|---|---|
/api/runs | GET | List runs. Query params: ?status=pending|running|completed|failed|cancelled, ?signalName=name. Sorted by createdAt descending. |
/api/runs/stats | GET | Aggregate run counts by status. Returns { pending, running, completed, failed, cancelled }. |
/api/runs/:id | GET | Get a single run’s full details including input, output, error, timing, and attempts. Returns 404 if not found. |
/api/runs/:id/steps | GET | Get step execution records for a run. Each step includes name, status, input, output, error, and timestamps. |
/api/runs/:id/logs | GET | Get captured stdout/stderr log lines for a run. |
/api/runs/:id/cancel | POST | Cancel a run. Returns 403 in read-only mode, 400 if the run cannot be cancelled. |
Broadcasts
| Endpoint | Method | Description |
|---|---|---|
/api/broadcasts | GET | List all registered broadcasts with DAG structure — node names, signal mappings, dependency edges, failure policy. |
/api/broadcasts/:name | GET | Get a single broadcast’s metadata and DAG structure. Returns 404 if not found. |
/api/broadcasts/:name/trigger | POST | Trigger a broadcast with optional input. Body: { "input": { ... } }. Returns the new broadcast run ID. Returns 403 in read-only mode. |
/api/broadcasts/:name/runs | GET | List all runs for a specific broadcast. |
/api/broadcast-runs/:id | GET | Get a broadcast run’s full details. Returns 404 if not found. |
/api/broadcast-runs/:id/nodes | GET | Get all node runs for a broadcast execution. Each node includes name, signal name, signal run ID, status, skip reason, input, output, error, and timestamps. |
/api/broadcast-runs/:id/logs | GET | Get aggregated logs from all node signal runs in a broadcast execution. Sorted by timestamp. |
/api/broadcast-runs/:id/cancel | POST | Cancel a broadcast run. Returns 403 in read-only mode, 400 if it cannot be cancelled. |
WebSocket events
Connect to /api/events on the API server port. Each message is a JSON object with type, timestamp, and data fields.
Signal events
| Event type | Description |
|---|---|
signal:discovered | A signal file was found during directory scanning. |
run:dispatched | A run was picked up from the queue and dispatched for execution. |
run:started | A run’s handler began executing in the child process. |
run:completed | A run finished successfully. Includes output data. |
run:failed | A run failed after exhausting all retry attempts. Includes error message. |
run:timeout | A run exceeded its timeout and was killed. |
run:retry | A run failed but has remaining attempts. Includes current attempt and max attempts. |
run:cancelled | A run was cancelled via the API or programmatically. |
run:skipped | A recurring run was skipped (e.g. previous run still active). |
run:rescheduled | A recurring run was rescheduled. Includes the next run time. |
step:started | A step within a multi-step signal began executing. |
step:completed | A step finished successfully. |
step:failed | A step failed. |
log:output | A line of stdout or stderr was captured from a running signal handler. Includes run ID, signal name, level, and message. |
run:completeError | An error occurred while trying to mark a run as complete (e.g. adapter failure during finalization). |
Broadcast events
| Event type | Description |
|---|---|
broadcast:discovered | A broadcast file was found during directory scanning. |
broadcast:queued | A broadcast run was added to the queue. |
broadcast:started | A broadcast began executing its DAG. |
broadcast:completed | All nodes in the broadcast finished successfully. |
broadcast:failed | The broadcast failed. Includes error message. |
broadcast:cancelled | The broadcast was cancelled. |
node:triggered | A DAG node’s signal was triggered for execution. |
node:completed | A DAG node’s signal completed successfully. |
node:failed | A DAG node’s signal failed. Includes error message. |
node:skipped | A DAG node was skipped. Includes the reason: guard (guard function returned false), upstream-failed (a dependency failed), or cancelled. |
Using Station with an existing runner
A common setup: one process runs signals, another runs Station for monitoring. Both point at the same SQLite database.
// runner.ts — executes signalsimport path from "node:path";import { SignalRunner } from "station-signal";import { SqliteAdapter } from "station-adapter-sqlite"; const runner = new SignalRunner({ signalsDir: path.join(import.meta.dirname, "signals"), adapter: new SqliteAdapter({ dbPath: "./jobs.db" }),}); runner.start();// station.config.ts — read-only monitoringimport { defineConfig } from "station-kit";import { SqliteAdapter } from "station-adapter-sqlite";import { BroadcastSqliteAdapter } from "station-adapter-sqlite/broadcast"; export default defineConfig({ port: 4400, signalsDir: "./signals", broadcastsDir: "./broadcasts", adapter: new SqliteAdapter({ dbPath: "./jobs.db" }), broadcastAdapter: new BroadcastSqliteAdapter({ dbPath: "./jobs.db" }), runRunners: false, // Don't execute signals — just monitor});Graceful shutdown
Station listens for SIGINT and SIGTERM. On shutdown, it stops the broadcast runner first (it may query the database during cleanup), then the signal runner, then closes the WebSocket server, log store, and HTTP server. Both runners are given a 5-second grace period to finish in-flight work.