PugBase/Docs

Webhooks - wake your agents

PugBase POSTs a signed event when the plan moves. A background or cloud agent is woken by it instead of asking next_action every minute.

Why

A long-running agent that waits for another feature to land has two choices today: poll next_action, or stop and be restarted by a person. Webhooks give it a third: an HTTPS endpoint of yours receives feature.ready (or task.reopened, review.requested...) and starts the agent with that context.

Events

feature.readyevery feature this one depended on is complete - an agent can start it now
feature.completeevery task note in a feature is done
task.donea task note reached 100% ticked
task.reopenedQA or a reviewer reopened a task, with the reason
handoff.pushedan agent pushed a result / handoff into a task note
review.requesteda pull request was opened for review
pingtest delivery from the settings page

Set up (owner)

  1. Workspace Settings > Webhooks > paste the URL of your receiver, pick the events, Add. Copy the signing secret - it is shown once.
  2. Press Test: PugBase sends a ping event; the row shows the HTTP status your endpoint answered with.
  3. Each delivery is retried twice on a network error or 5xx (not on 4xx). Deliveries are listed in the Audit log.

Verify the signature

Every request carries X-PugBase-Timestamp (ms) and X-PugBase-Signature = sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with your secret>. Reject a bad signature or a timestamp older than 5 minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPugbase(req, rawBody, secret) {
  const ts = req.headers["x-pugbase-timestamp"];
  const sig = req.headers["x-pugbase-signature"];
  if (!ts || !sig || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Payload

JSON with id, event, workspaceId, at, data and a hint that says what an agent should do next - the same words next_action uses. data carries noteId / noteTitle for note events and feature for feature events.

{
  "id": "6f0c...",
  "event": "feature.ready",
  "workspaceId": "5srXzYvCzloSZzcD7Fae",
  "at": "2026-10-31T09:12:44.120Z",
  "data": { "feature": "Checkout", "unblockedBy": "Catalog", "open": 6 },
  "hint": "Feature \"Checkout\" is unblocked. Call next_action (or check_ready with this feature) to claim its first task."
}

Starting an agent from a webhook

The receiver is yours: a small server, a serverless function, or a CI job trigger. The usual pattern is to spawn Claude Code in headless mode with the hint as the prompt - it then calls next_action itself and claims the work.

// Express receiver that wakes a headless Claude Code run
app.post("/pugbase", express.raw({ type: "*/*" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verifyPugbase(req, body, process.env.PUGBASE_WEBHOOK_SECRET)) return res.status(401).end();
  res.status(202).end(); // answer first - PugBase retries slow receivers
  const { event, hint } = JSON.parse(body);
  if (event === "ping") return;
  spawn("claude", ["-p", hint + " Read the PugBase server instructions first.", "--max-turns", "60"], {
    cwd: "/srv/project", env: process.env, stdio: "inherit",
  });
});

Agent teams / Team Agents

Limits

  • https only (http is accepted for localhost while developing); private addresses are refused.
  • At most 10 webhooks per workspace; a receiver that keeps failing stays registered - disable it with the switch or fix the endpoint.
  • A webhook is a nudge, not the source of truth: an agent that starts from one still goes through next_action / check_ready, which can say wait.
PugBase x Claude Code - plan it in PugBase, build it with Claude, review it together.