@pipedai/worker
v1.3.16
Published
PipedAI worker — registers with pipedai-api, polls for due runs, and invokes Claude with the trigger's MCP.
Readme
pipedai-worker
PipedAI worker — Node CLI that registers with pipedai-api, polls for
runs assigned to this machine, and invokes claude -p with the trigger's
MCP config. Distributed via npm as @pipedai/worker. Runs on
Marolence-owned and BYOM (Bring-Your-Own-Machine) client hosts.
See ARCHITECTURE.md for the runtime model and CLAUDE.md for working
guidelines.
Quick install (one-liner)
Once you have a wrk_ registration token from the dashboard's
Settings → API Keys → Worker Registration tab:
npm i -g @pipedai/worker \
&& pipedai-worker register --token=wrk_… --api-url=https://api-beta.pipedai.app \
&& pipedai-worker runFor Claude Max mode, run claude login interactively first. For
api-key mode, export ANTHROPIC_API_KEY instead and pass
AUTH_MODE=api-key to register.
pipedai-worker doctor runs a diagnostic checklist (claude install,
auth, network, file permissions) and prints actionable hints for any
check that fails — start there if registration or run-loop misbehaves.
Windows (PowerShell / cmd)
Same npm i -g @pipedai/worker command — npm installs a pipedai-worker.cmd
shim on %PATH%. Run from an elevated PowerShell or cmd window. Requires
worker 1.3.6+ (older builds had two Windows-specific bugs: mkdir ''
during register, and spawn claude ENOENT during run). The worker config
lands under %USERPROFILE%\.pipedai\worker.json. claude login works the
same way as on macOS / Linux.
Stack
- Node 22 + TypeScript 5.9
commanderfor arg parsingtsupbundles to a single executabledist/cli.jszodfor input validation- Vitest, ESLint, Prettier
- mise pins Node 22
Local Setup
Clone all three pipedai repos as siblings (
pipedai-api,pipedai-website,pipedai-worker) under one parent directory.Install mise (or install Node 22 directly):
brew install mise mise install # picks up mise.toml -> node@22Install dependencies and build:
npm install npm run build # emits dist/cli.js with #!/usr/bin/env node shebang(One-time) install the binary globally so
pipedai-workeris on$PATH:npm install -g .v2 will replace this with
npm install -g @pipedai/workerfrom the public registry.
Subcommands
pipedai-worker register --token <wrk_…> --api-url <https://…> [--hostname <host>]
pipedai-worker run [--config <path>]
pipedai-worker doctor [--api-url <url>]
pipedai-worker versionregister
Trades a wrk_ workspace registration token for a persistent wkt_
worker token, and writes the credentials to ~/.pipedai/worker.json
(chmod 600).
AUTH_MODE=claude-max pipedai-worker register \
--token wrk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--api-url https://pipedai-api-production.up.railway.appAUTH_MODE accepts claude-max (default) or api-key. Use api-key
on machines where Claude Code is configured with ANTHROPIC_API_KEY
instead of a Max subscription.
Re-running register is idempotent — it preserves the same workerId
and rotates the wkt_ token (useful if the local worker.json was
lost or compromised).
Exit codes: 0 success, 1 API rejected (401/404/4xx), 2 local error
(network unreachable or invalid AUTH_MODE).
Multiple environments on one host
To register the same machine against more than one environment (e.g. beta
and production at once), pass --config <path> so each registration
writes to its own file instead of overwriting the default:
pipedai-worker register \
--token wrk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--api-url https://api-beta.pipedai.app \
--config ~/.pipedai/worker-beta.json
pipedai-worker register \
--token wrk_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy \
--api-url https://api.pipedai.app \
--config ~/.pipedai/worker-prod.jsonThen start one run process per config (each its own poll loop, own
CONCURRENCY budget, own service file). See "Running as a Service"
below for the service-file pattern — copy the launchd / systemd template
once per environment, change the Label / Description, and append
--config ~/.pipedai/worker-<env>.json to the run command.
Pool worker registration (--pool)
Marolence-managed pool workers serve any workspace's usePool=true
triggers via a shared global queue. Registration takes a pool_*
token (admin-minted via POST /api/v1/admin/pool-workers/api-keys,
platform-admin-only — wrk_* tokens are rejected by the pool
endpoint, and vice versa).
pipedai-worker register \
--pool \
--token pool_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--api-url https://api.pipedai.app \
--hostname studio-pool-1 \
--config ~/.pipedai/worker.pool-1.jsonPoolWorker.hostname is @unique on its own (no workspace
disambiguation — pool workers are scopeless), so to register multiple
pool workers on the same host you MUST pass distinct --hostname
values (e.g. studio-pool-1, studio-pool-2, studio-pool-3).
Each one needs its own --config path + its own LaunchAgent /
systemd unit.
The post-register success message shows scope: pool (Marolence-managed)
instead of workspace: … so it's obvious at a glance which kind of
registration landed. Pool workers serve every workspace, not just one.
run
Reads ~/.pipedai/worker.json and enters the poll loop. Press Ctrl-C
once to stop after the current run finishes; press it again to force-exit.
POLL_INTERVAL_MS=60000 pipedai-worker run| Env | Default | Notes |
|---|---|---|
| POLL_INTERVAL_MS | 60000 | Floor 1000. Smaller values risk hot-spinning the API. |
Pre-flight check
Optional per-trigger HTTP gate the worker consults before invoking
claude -p. The customer endpoint returns {run: false} to skip
the Claude invocation entirely — useful for triggers whose work is
event-conditional ("only run if there are new emails to process").
Saves tokens on cron triggers that fire on a fixed schedule but
sometimes have nothing to do.
Configured per-trigger in the dashboard (Pre-flight URL + optional
headers + optional HMAC signing secret + on-demand toggle). The
worker reads the four fields from /poll alongside the existing
MCP config.
Wire shape (worker → your endpoint):
POST {preflightUrl}
X-Pipedai-Trigger-Id: trg_abc
X-Pipedai-Run-Id: run_xyz
X-Pipedai-Signature: sha256=<hex(hmac-sha256(rawBody, preflightSecret))>
Content-Type: application/json
(+ any headers from preflightHeaders)
{
"triggerId": "trg_abc",
"runId": "run_xyz",
"scheduledAt": "2026-05-27T15:00:00.000Z",
"lastRunAt": "2026-05-27T14:30:00.000Z",
"lastRunStatus": "succeeded",
"attempt": 1,
"source": "cron"
}Expected response:
200 {"run": true}→ worker proceeds toclaude -pas normal.200 {"run": false, "reason": "no new emails"}→ run lands asstatus='skipped'with thereasonstring surfaced in the dashboard's run-history detail. The worker NEVER spawns Claude.- Anything else (non-2xx, 30s timeout, unreachable, malformed JSON
body, missing
runfield) → run lands asstatus='failed'withfaultClass='gate_check_failed'. The api retries within the trigger'smaxRetriesbudget the same way an infra fault would.
Tiny example (Node http — a real customer endpoint would
probably check a database):
import http from 'http';
http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', async () => {
const ctx = JSON.parse(body);
const hasWork = await checkDatabaseForPendingItems(ctx.triggerId);
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(
hasWork ? { run: true } : { run: false, reason: 'nothing pending' }
));
});
}).listen(3000);Verifying the signature:
import { createHmac } from 'crypto';
function verify(rawBody, signatureHeader, secret) {
const expected = `sha256=${createHmac('sha256', secret)
.update(rawBody).digest('hex')}`;
return expected === signatureHeader;
}The HMAC is computed over the raw body bytes — verify BEFORE
parsing JSON, since JSON.parse(rawBody) then JSON.stringify(...)
can reorder keys and mismatch.
On-demand bypass: when an operator fires the trigger via the
dashboard's "Run now" button (or POST /triggers/:id/run via API
key), the worker skips the pre-flight check by default — on-demand
means "I want this NOW". Customers who want gating on every run,
including on-demand, flip the trigger's "Also gate on-demand runs"
toggle.
30s timeout: hardcoded. If your gate logic can't decide in 30s, the answer should be pre-computed somewhere cheaper (e.g. a flag that a separate job sets when new work arrives).
Running as a Service
The worker is a long-running process — keep it alive with the host's
service manager. Sample config files for the four common surfaces ship
in templates/; copy and edit per host.
systemd (Linux)
sudo cp templates/systemd-pipedai-worker.service /etc/systemd/system/pipedai-worker.service
sudo $EDITOR /etc/systemd/system/pipedai-worker.service # set User= and AUTH_MODE / ANTHROPIC_API_KEY
sudo systemctl daemon-reload
sudo systemctl enable --now pipedai-worker
journalctl -u pipedai-worker -fThe template uses KillSignal=SIGINT and TimeoutStopSec=120 so the
worker has time to finish its in-flight run before systemd escalates.
launchd (macOS)
cp templates/launchd-com.marolence.pipedai-worker.plist \
~/Library/LaunchAgents/com.marolence.pipedai-worker.plist
$EDITOR ~/Library/LaunchAgents/com.marolence.pipedai-worker.plist # set AUTH_MODE / ANTHROPIC_API_KEY
launchctl load -w ~/Library/LaunchAgents/com.marolence.pipedai-worker.plist
launchctl list | grep pipedai-worker
tail -f /tmp/pipedai-worker.out.lognssm (Windows)
choco install nssm
copy templates\windows-nssm-install.bat .
notepad windows-nssm-install.bat :: edit AUTH_MODE / paths
:: Open an Administrator command prompt and run:
windows-nssm-install.bat
nssm start PipedAIWorkerDocker / docker-compose
cp templates/Dockerfile templates/docker-compose.yml ./
ANTHROPIC_API_KEY=sk-... docker compose up -d
docker compose logs -f pipedai-workerThe compose file bind-mounts ~/.pipedai so the container reads the
host's worker.json — register on the host first with
pipedai-worker register --token=… --api-url=… before bringing the
container up.
npm Scripts
| Script | Purpose |
|---|---|
| npm run dev | Run the CLI directly via tsx (no build). Useful for npx tsx src/cli.ts <subcommand>. |
| npm run build | tsup bundle to dist/cli.js. |
| npm start | node dist/cli.js. Equivalent to running the bin. |
| npm run type-check | tsc --noEmit. |
| npm run lint | ESLint over src/. |
| npm run format | Prettier write-in-place. |
| npm test | Run the Vitest suite once. |
| npm run test:watch | Vitest watch mode. |
Multi-Repo Workflow
Phases, tasks, and tickets live in the Marolence Developer Portal MCP,
not in this repo. There is no PLAN.md. See CLAUDE.md (managed centrally
— don't hand-edit) for how to find your work, propose scope changes, and
follow the commit conventions that drive task attribution and billing.
When a task spans repos, navigate to ../pipedai-api or ../pipedai-website
and make changes there directly — they're sibling clones in the same parent
directory.
