SupaNet
Building on SupaNet

Webhooks

Four modes for letting outside systems trigger work in SupaNet — save to table, call a function, run an agent, or run a prompt.

A webhook lets an external system POST data into SupaNet and have something happen. Each webhook is a row with an opaque token; callers hit the public webhook edge function at /functions/v1/webhook/<token>.

Four modes, in precedence order

When a payload arrives, the webhook function decides what to do based on how the webhook is configured. Exactly one target is chosen (they are mutually exclusive). The precedence is:

  1. Save to table (table_id + target_column) - deterministic, no model. The raw JSON payload drops straight into one column of a user table — it never skips, never gets lost, and the AI has no say. If the target table is an event source (has emit_events enabled), the row insert triggers a table.* event that any listener can react to. This is the "append-only ingestion" pattern: external data flows in, a database fact is recorded, and then intelligent processing happens downstream via listeners.
  2. Direct tool (tool_id) - deterministic, no model. The payload is validated against the tool's input schema (required fields and top-level types), then POSTed straight to the tool's URL. A bad payload returns a 400 listing the offending fields. This is the "n8n function node" pattern: schema validation is the gate, because nothing reaches an LLM.
  3. Agent (agent_id) - the payload runs through an agent (its prompt and tools) via the agent loop.
  4. Prompt - the payload runs against a plain prompt.

Per-table event sourcing

When you enable "Trigger an event when a row is added" on a table (via Settings), the table becomes an event source. Every new row insert fires a table.<slug>_<id> event carrying the full row as data. The event is automatically scoped to the table's visibility (private or workspace).

Each table gets a stable, frozen event type — once minted, toggling events on/off preserves it, so listeners wired to the event name never break from a pause/resume.

The webhook → table → listener flow:

  1. Webhook saves raw payload to table (deterministic, no guardrail).
  2. Row insert fires a table.* event.
  3. A listener matches the event and runs an agent (with the event data as context) to structure, classify, or act on the raw data.
  4. The agent files the processed result into collections for your team.

This decouples ingest from processing: raw data always lands, processing can be retried/modified/scoped without changing the webhook, and the event log gives you a full audit trail.

Read-only by default

A webhook-targeted agent runs read-only unless allow_tools = true. Tools are only loaded when that flag is set - a deterministic rule in code, not a model decision - so an untrusted source cannot make the agent take actions.

Guardrails on the LLM paths

The agent and prompt modes run a guardrail pre-flight, and webhooks fail closed: if the guardrail errors or blocks, the run is stopped and logged as blocked (HTTP 403). The direct-tool mode skips this, because no LLM is involved - schema validation already gates it.

Optional shared secret

The URL token alone is "secret URL" security. For real authentication, set a secret on the webhook. Callers must then present it as Authorization: Bearer <secret> or X-Webhook-Secret: <secret>, or the function returns 401 before logging anything - so a wrong or missing secret cannot spam the event log. A null secret means no secret (unchanged behaviour). It is a plaintext shared secret on the row, the same trust model as the token.

The event log

Every call writes a webhook_events row (receivedok / error / blocked) with the result. The Webhooks page subscribes to these over Realtime for a live log. Outcomes also land in the activity log.

Pairing with Forge

Because forged functions are http tools, the editor's "call a function directly" picker lists them, shows their fields and types, and renders a sample payload - making the deterministic direct-tool mode easy to wire up.

On this page