pboss logo pboss.docs
Menu

Guides

Programmatic API

Drive pboss from TypeScript — zero-ceremony imports, the PBoss singleton, events, error handling, and direct ProcessManager usage.

pboss exposes a complete programmatic API. A single machine only needs one instance of the process manager — every API function, static method, and singleton instance automatically communicates with that single machine-level daemon.

Zero-ceremony quick start#

Import and call API methods directly — no initialization, construction (new), or connection ceremony:

// 1. Direct function exports (zero initialization needed)
import { getProcesses, list, describe, start, stop, restart } from "pboss";

// Read existing processes immediately
const processes = await getProcesses();
console.log(processes);

// Start, stop, or manage processes
await start({ script: "./server.ts", name: "api", instances: 4, port: 3000 });
await restart("api");
// 2. Default singleton import
import pboss from "pboss";

const procs = await pboss.list();
const metrics = await pboss.metrics();
// 3. PBoss class & static methods
import PBoss from "pboss";

const procs = await PBoss.list();
const info = await PBoss.describe("api");

Reading processes without a daemon#

Two helpers read state without spawning anything:

  • getProcesses(): Promise<ProcessState[]> — if the daemon is active, returns live processes; if it’s offline, automatically reads the saved process definitions from disk (~/.pboss/dump.json) without spawning a daemon.
  • readSavedProcesses(): Promise<ProcessState[]> — directly parses ~/.pboss/dump.json with zero daemon or socket involvement.
import { getProcesses } from "pboss";

const processes = await getProcesses();
for (const p of processes) {
  console.log(`${p.name} (id: ${p.pm_id}) - ${p.status}`);
}

Connection lifecycle#

All API methods auto-connect on demand, so explicit connection is optional — it only matters if you want to listen for lifecycle events before issuing commands.

  • pboss.connect(): Promise<PBoss> — connect to the daemon; spawns it if it isn’t running and waits up to 5 seconds for responsiveness.
  • pboss.disconnect(): Promise<void> — disconnect; stops polling timers but does not kill the daemon or your processes.
  • pboss.connected: boolean — read-only connection state.
  • pboss.daemonPid: number | null — PID of the daemon, or null before connecting.
import PBoss from "pboss";

const pboss = new PBoss();
await pboss.connect();
console.log(`Connected to daemon PID ${pboss.daemonPid}`);

API surface#

Every CLI command has a method twin:

AreaMethods
Process controlstart(options), startTarget(target), startEcosystem(config), stop(target?), restart(target?), reload(target?), del(target?) / delete(target?, opts?), scale(target, count), sendSignal(target, signal), reset(target?)
Introspectionlist(), describe(target), deps(target), logs(target?, lines?), streamLogs(target, cb, signal?), flush(target?)
Monitoringmetrics(), metricsHistory(seconds?), prometheus(), startPolling(intervalMs?), stopPolling()
Persistencesave(), resurrect()
Dashboarddashboard(port?, metricsPort?), dashboardStop()
ModulesmoduleInstall(nameOrPath), moduleUninstall(name), moduleList()
Daemonping(), kill(), daemonReload()
Low-levelsend(message: DaemonMessage) — arbitrary daemon messages over the Unix socket

Targets accept a namespace everywhere — "my-app" operates on the whole group, with a clear not-found error for unknown targets instead of a silent no-op. startTarget resumes stopped members by name or namespace without creating anything new — a namespace resume is atomic (#31): if any member fails, only the members that call started are rolled back:

await pboss.startTarget("stellarforge"); // every stopped member comes back online — atomically

The same atomicity governs startEcosystem (standalone apps independent, each namespace one atomic group) and namespace-level restart — see Processes — Namespaces. start(options) accepts onNsMemberExit: "ignore" | "exit", the programmatic form of the --on-ns-member-exit flag.

start(options) is dependency-aware (#33): dependsOn: ["postgres", { name: "metrics", policy: "optional" }] is resolved before the process spawns — pboss apps first (stopped ones are started), then systemd units (checked, never managed). A blocked start throws a PBossError whose response.dependencyFailure carries the structured facts:

try {
  await pboss.start({ script: "./api.ts", name: "api", dependsOn: ["postgresql"] });
} catch (err) {
  // err.response.dependencyFailure:
  // { process: "api", dependency: "postgresql", provider: "systemd",
  //   service: "postgresql.service", state: "inactive",
  //   reason: "dependency_not_running" }
}

deps(target) inspects the graph without starting anything, and delete(target, { force: true }) overrides the has-dependents refusal:

const reports = await pboss.deps("api");
for (const dep of reports[0].dependencies) {
  console.log(dep.name, dep.provider, dep.status, dep.satisfied);
}

streamLogs is the programmatic tail:

await pboss.streamLogs("my-api", (log) => {
  console.log(log.name, log.out ?? log.err);
}, abortController.signal);

Events#

The PBoss class extends EventEmitter. Since issue #32, process:* events are real daemon state changes, not echoes of your own calls: every event originates in the daemon’s ProcessManager and reaches every subscribed client over a persistent stream that connect() opens. If client A restarts a process, client B hears process:restart; a crash-triggered autorestart fires events nobody asked for.

Each process:* event carries one PbossProcessEvent (one event = one process):

FieldDescription
eventThe event name (identical to the key you subscribed to)
sourceWhat caused it: user, crash, memory, watch, cron, health, policy, or system (boot resurrect)
atEpoch milliseconds at emit time
processFresh ProcessState snapshot of the affected process
exitCode / exitSignalprocess:crashed — raw exit facts
willRestartprocess:crashed — was an autorestart scheduled?
reasonHuman-readable detail (e.g. the give-up reason on process:errored)
EventPayloadDescription
daemon:connected—Daemon connection established
daemon:disconnected—Client disconnected, or the stream ended because the daemon died
daemon:launchedpid: numberDaemon was spawned by this client
daemon:killed—Daemon was killed via kill()
errorerror: ErrorTransport or polling error
process:startevent: PbossProcessEventA process actually came online (start / resume / resurrect / scale-up)
process:stopevent: PbossProcessEventpboss deliberately stopped it (user op or namespace policy)
process:restartevent: PbossProcessEventBack online after a restart — manual or autonomous, see source
process:crashedevent: PbossProcessEventExited on its own (NOT a pboss stop); check exitCode / willRestart
process:erroredevent: PbossProcessEventTerminal: start failed or the restart budget was exhausted
process:deleteevent: PbossProcessEventRemoved from pboss’s list
process:reloadevent: PbossProcessEventA graceful reload completed
metricssnapshot: MetricSnapshotMetrics snapshot received (client-side polling)
log:datalogs: Array<{ name, id, out, err }>Log data retrieved

The synthetic process:scale echo is gone: scale-ups arrive as one process:start per new instance, scale-downs as process:stop + process:delete per removed instance.

import PBoss from "pboss";

const pboss = new PBoss();

pboss.on("process:crashed", (e) => {
  console.log(`${e.process.name} exited (code ${e.exitCode ?? "signal " + e.exitSignal})`);
});
pboss.on("process:restart", (e) => {
  console.log(`${e.process.name} is back online (${e.source} restart)`);
});
pboss.on("error", (err) => console.error("pboss error:", err.message));

await pboss.connect(); // opens the event stream; events flow from here

The stream opens automatically with connect(). Call await pboss.subscribeEvents() manually if you use the request-style helpers without connecting. disconnect() closes the stream; the daemon detaches the subscription when the socket dies, so nothing leaks on either side.

Error handling#

Methods that communicate with the daemon throw a PBossError when the daemon returns a failure response. The error includes the command that failed and the full daemon response. Transport-level errors (daemon unreachable, socket closed) throw standard Error instances.

import { PBossError } from "pboss";

try {
  await pboss.describe("nonexistent");
} catch (err) {
  if (err instanceof PBossError) {
    console.error(`Command "${err.command}" failed: ${err.message}`);
    console.error("Full response:", err.response);
  }
}
PBossError propertyTypeDescription
messagestringHuman-readable error message
commandstringThe daemon command type that failed
responseDaemonResponseThe full response object from the daemon

Direct ProcessManager usage#

For in-process usage without a running daemon, use ProcessManager directly — useful for embedding pboss into your own application or for custom tooling:

import { ProcessManager, Dashboard } from "pboss";

const pm = new ProcessManager();

// Start a process
const states = await pm.start({
  name: "my-api",
  script: "./server.ts",
  instances: 4,
  execMode: "cluster",
  port: 3000,
  env: { NODE_ENV: "production" },
  maxMemoryRestart: "512M",
  healthCheckUrl: "http://localhost:3000/health",
});

console.log("Started:", states.map((s) => `${s.name} (pid: ${s.pid})`));

// List processes
const list = pm.list();

// Get metrics
const metrics = await pm.getMetrics();

// Scale
await pm.scale("my-api", 8);

// Graceful reload
await pm.reload("my-api");

// Start the web dashboard
const dashboard = new Dashboard(pm);
dashboard.start(9615, 9616);

// Get Prometheus-format metrics
const promText = pm.getPrometheusMetrics();

// Save and restore
await pm.save();
await pm.resurrect();

// Stop everything
await pm.stopAll();

The ProcessManager provides the same process management capabilities but runs in-process rather than communicating with a daemon. Use the PBoss client class for the standard daemon-based workflow, and ProcessManager when you need direct, embedded control. It’s also the object modules receive in their init hook.