Delegate long-running agent tasks to a Raspberry Pi and get signed webhook callbacks when each job completes.
Keep the main chat/session responsive by offloading async tasks (daily briefs, scraping, summarization, indexing, transforms) to a queue-backed worker running on your Pi.
- ✅ REST API to enqueue/query jobs
- ✅ Redis queue with retries (BullMQ)
- ✅ Node.js worker with allowlisted job handlers
- ✅ Signed webhook callbacks (
job.completed,job.failed) - ✅ Cron scheduler service
- ✅ Built-in pilot job:
daily-crypto-brief - ✅ Markdown artifacts written to local storage
apps/api– Fastify API (POST /jobs,GET /jobs/:id, lifecycle ops)apps/worker– BullMQ worker executes registered jobsapps/scheduler– cron triggers recurring jobsapps/installer– syncssystemdmanifests into unit/timer files (dry-run or host-apply)packages/core– queue + schema shared codepackages/jobs– job registry + task implementationspackages/notifier– webhook signing + delivery/retrydocs/– quickstart, security, webhook contract, job authoring
cp examples/.env.example .env
# edit values:
# RUNNER_API_TOKEN=...
# WEBHOOK_SECRET=...
# CALLBACK_URL=...docker compose -f deploy/docker-compose.yml up -dcurl -X POST http://localhost:8787/jobs \
-H "Authorization: Bearer $RUNNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jobType":"daily-crypto-brief",
"input":{"maxItems":5}
}'curl -H "Authorization: Bearer $RUNNER_API_TOKEN" \
http://localhost:8787/jobs/<jobId>Artifacts are written to:
data/artifacts/Body:
{
"jobType": "daily-crypto-brief",
"input": { "maxItems": 5 },
"priority": 5,
"callbackUrl": "https://example.com/runner-callback",
"metadata": { "requestedBy": "agent-main" },
"scheduleBackend": "bullmq",
"idempotencyKey": "optional-client-key"
}Supports optional schedule:
{
"schedule": {
"runAt": "2026-03-10T13:30:00+05:30",
"everySeconds": 3600,
"cron": "0 9 * * 1-5",
"tz": "Asia/Kolkata"
}
}Rules:
- Use only one repeat mode:
everySecondsorcron runAtmust be in the futureeverySeconds/croncreate repeating jobs
Backends:
scheduleBackend: "bullmq"(default) keeps scheduling inside Redis/BullMQscheduleBackend: "systemd"writes a signed/structured manifest underdata/systemd-manifests/for host-level timer installers- installer supports cron patterns:
M * * * *,M H * * *,M H * * D
- installer supports cron patterns:
Idempotency:
- pass
Idempotency-Keyheader (or bodyidempotencyKey) to dedupe retries
Returns job state, attempts, result/error, timestamps.
Returns latest jobs (basic listing).
Returns recurring/system schedules tracked by the API (BullMQ + systemd-manifest backends).
Pause/resume a tracked schedule:
- BullMQ backend: removes/re-adds repeatable schedule
- systemd backend: updates manifest
enabledflag; installer disables/enables the corresponding timer
{ "action": "pause" }or
{ "action": "resume" }Remove a queued job or tracked schedule.
- systemd backend also deletes manifest; installer prunes stale
piar-sysd-*unit/timer files.
Returns waiting/active/completed/failed/delayed counters.
apps/installer polls data/systemd-manifests/ and syncs generated unit/timer files.
- writes units to
data/systemd-units/ - does not call
systemctl
Run installer on host with:
APPLY_SYSTEMD=trueSYSTEMD_DIR=/etc/systemd/systemINSTALLER_TOKENset (same value in API + installer)
Minimal host service pattern:
[Service]
EnvironmentFile=/path/to/pi-agent-runner/.env
Environment=RUNNER_DATA_DIR=/path/to/pi-agent-runner/data
Environment=APPLY_SYSTEMD=true
Environment=SYSTEMD_DIR=/etc/systemd/system
Environment=API_URL=http://127.0.0.1:8787
ExecStart=/usr/bin/node /path/to/pi-agent-runner/apps/installer/src/index.jsThe generated service triggers:
POST /internal/systemd/trigger/:id
That endpoint is installer-only and guarded by x-installer-token when INSTALLER_TOKEN is set.
- Bearer-token API auth
- HMAC SHA256 signed callbacks
- Bounded callback retries
- Job allowlist (no arbitrary shell execution in v1)
See docs/security.md and docs/webhook-contract.md.
Scheduler uses CRYPTO_BRIEF_CRON to enqueue daily-crypto-brief.
Worker fetches RSS from configured crypto sources, picks latest top items, and writes:
data/artifacts/crypto-brief-YYYY-MM-DD.mdThen sends webhook callback with summary + artifact path.
Use jobType: "reminder" when users ask to be reminded at a specific date/time or at a recurring frequency.
One-time reminder:
curl -X POST http://localhost:8787/jobs \
-H "Authorization: Bearer $RUNNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jobType": "reminder",
"input": {
"title": "Follow-up",
"message": "Ping Alice about the deployment",
"recipient": "megabyte"
},
"schedule": {
"runAt": "2026-03-10T17:00:00+05:30"
}
}'Recurring reminder (every 6 hours):
curl -X POST http://localhost:8787/jobs \
-H "Authorization: Bearer $RUNNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jobType": "reminder",
"input": {
"title": "Hydration",
"message": "Drink water",
"recipient": "megabyte"
},
"schedule": {
"everySeconds": 21600
}
}'When it executes, the worker completes the reminder job and callback relay forwards a concise reminder message to Telegram.
- callback delivery dead-letter queue
- Postgres storage adapter (optional)
- signed callback verifier example service
- multi-job templates (web-monitor, PDF batch, digest)
-
PATCH /jobs/:idschedule editing (not only pause/resume) - installer
/statusendpoint + sync metrics
MIT