# Workflow triggers — open webhook decisions

Status: idea. The framing is settled in [durable-workflows § Triggers — one door, not a plane](./durable-workflows.md#triggers--one-door-not-a-plane): triggers aren't a plugin plane, they're callers of one door (`wf_spawn(id, payload)`). This note only holds the decisions the inbound-webhook door still forces — read that section first.

## Goal

Let a workflow author declare an external caller: a stable signed URL that an outside system (GitHub, Linear) hits to spawn a run, its body becoming the payload. Schedule (built) and manual spawn (built) already cover the other callers. The webhook is the one seam left.

## Settled (don't relitigate)

- **No `triggers[]` list, no `api` kind.** Manual spawn with a payload (`POST /api/workflows`, bot-key authed) already *is* the API trigger — a second per-workflow token would duplicate the house key. Schedule stays its own column + pg_cron mirror; don't migrate it for symmetry.
- **Payload is pass-through.** The body lands in payload untouched; recipes shape it with `{{path}}`, rendered in the conductor, unresolved paths fail early. No ingest-time mapping DSL.

## Open decisions (the webhook door forces these)

- **Storage.** The webhook caller needs a row, not a jsonb element: stable id (the URL embeds it), `enabled`, rotate/revoke, a rate cap, `last_fired_at`. You can't cleanly disable the Nth element of an array.
- **Signing secret is trigger-owned, not a house secret.** House secrets are name-unique, sandbox-injected, member-readable — a signing secret is none of those. Reuse Vault *storage*, verify server-side like `x-conductor-secret` / `hashApiKey`; don't put it in the `secrets` namespace.
- **Idempotency.** Webhooks retry — derive Absurd's idempotency key from the delivery id (`X-GitHub-Delivery`) so a redelivery doesn't double-spawn. `wf_spawn` takes no key today, so this shapes its signature now.
- **Why-a-run-fired.** Record trigger ref + raw inbound body on the run so `arbe wf show` can say "fired from the GitHub webhook at 04:02 with this body."

## Ship-gate

Per-trigger rate cap is a gate, not a caution: the webhook door doesn't ship enabled without a count window in the trigger row returning 429 past it. With no external users, the real failure is our own runaway loop, and one hit = one sandbox run = real money.

See [durable-workflows](./durable-workflows.md), [system/secrets](../system/secrets.md), [workflows](../workflows.md).
