Pipeline Apps

Webhook Relay

A gallery app that records inbound webhook events, matches them to relay rules and tracks each delivery's status on the shared host.

Webhook Relay keeps a list of relay rules. Each rule matches recorded webhook events by event type and names a target URL. For every matched event you can queue a delivery and then record its outcome. The app ships no dedicated host page, renderer component or app-specific runtime branch. Its source is apps/webhook-relay/app.toml; the native records are in crates/shared/schema/app-webhook-relay/src/lib.rs.

This release records delivery state. It does not send the outbound HTTP request itself, and nothing runs automatically when an event is written. The friction log explains which parts of the app contract stop it.

Supported workflow

The relay starts empty. New rule opens a dialog that takes a name, a source label, the event type to match and a target URL. New rules are enabled.

The Inbound events form on the home page records an event: source, event type and a raw JSON payload. The server sets the receive time. A person or an agent with the app's write grant fills this in; there is no public webhook endpoint.

Open on a rule shows its page:

  • Find matching events lists the 50 newest events whose type equals the rule's event type.
  • Queue delivery on a matched event records a pending delivery for that event under the rule. Paused rules hide this button.
  • Show deliveries lists the rule's deliveries, newest first. Each one has Mark delivered, Retry (status retrying) and Record failure, which stores an error message with status failed. The list reloads after every change.
  • Pause rule and Resume rule switch enabled. Edit rule changes the name, source, event type and target URL. Delete rule hides the rule.

One package, existing host

Package declarationShared implementation
Native RelayRule, WebhookEvent and Delivery schemasDescriptor validation, typed CBOR records and per-field merge
relay-rule-list pipelineAuthorized dataset read, filters, deterministic sort and projection
event-matches and delivery-list pipelinesThe invoke_pipeline action with one signed input binding, applied as an overlay filter
Rule, event and delivery actionsDurable entity operations, optimistic projection for rules and explicit field masks
Write-then-read delivery actionsOne action flow chaining apply_entity_operation into invoke_pipeline
foreground_snapshot_v1The installed host's bounded foreground refresh scheduler

The release requests read and write access to app.webhook_relay.RelayRule, app.webhook_relay.WebhookEvent and app.webhook_relay.Delivery, and invocation of the three pipelines. Like the other app.* datasets, these are served by DES's portable app.* adapter and read owner-scoped. The release requests no network, secret, background, notification or infrastructure capability.

Author, build and install

rac app validate apps/webhook-relay/app.toml
rac app build apps/webhook-relay/app.toml
rac des spec publish --manifest apps/webhook-relay/pipelines/relay-rule-list.toml
rac des spec publish --manifest apps/webhook-relay/pipelines/event-matches.toml
rac des spec publish --manifest apps/webhook-relay/pipelines/delivery-list.toml
rac app publish apps/webhook-relay/app.toml
rac app install webhook-relay --version 0.1.0 \
  --installation-id webhook-relay-example \
  --grant relay-read,relay-write,relay-pipelines

Publication requires the same registry authority and grant review described in the release lifecycle. Only build/app-release.json is checked in.

Bounds

The rule list returns at most 200 live rules, sorted by name. Matching returns the 50 newest events of one type, and the delivery list returns 100 rows for one rule. Match and list filters are fixed when the pipelines are signed; the only value chosen at run time is the rule's event type or identity.

Friction log

These are the problems we hit while building the app. We describe them here without filing tickets. The ticket asked for ingest, routing, compute-on-write and outbound delivery with retry. Ingest and routing work; the other two do not fit the current contract.

  • No inbound webhook endpoint. The app TOML's top-level keys are app, agent, deployment, background_jobs, entities, pipelines, functions, capabilities, actions, views, assets and dependencies (crates/shared/schema/app-manifest/src/toml_projection.rs:15-38). None of them declares an HTTP ingress route. Events are therefore written through the authenticated entity write path, from the app form or by an agent with the write grant. A third party cannot POST to the app.
  • No compute-on-write. The same key list has no write trigger, so recording an event cannot start matching or queue deliveries. The nearest thing is background_jobs: a signed schedule that invokes a declared pipeline every 1 minute to 30 days (crates/shared/schema/app-manifest/src/background.rs:26-31). That is polling, not a reaction to a write, so we left it out of this minimal loop. Matching runs when someone presses Find matching events.
  • Outbound egress exists but cannot record its own result. A pipeline can contain an http_fetch stage with a method and body template. Release resolution requires a network_request capability that covers the stage's url_template (crates/shared/schema/app-manifest/src/resolve/pipeline_validation.rs:586-603). Three things kept it out of this app:
    1. Capability patterns are exact strings or *-suffixed prefixes (crates/shared/schema/app-manifest/src/validate/pins.rs:24-30) checked against the signed template. A per-rule, user-entered target_url needs either one host fixed at signing or a * pattern that allows egress to any host.
    2. DES rejects storage sinks on HTTP nodes ("HTTP pipeline node storage sinks require a publication workflow", crates/service/data-engine-service/src/pipeline_executor_runner.rs:88-94). A delivery pipeline therefore cannot write the Delivery outcome itself. A client action would have to read the response and write it back.
    3. We did not verify that a portable invoke_pipeline from the browser host reaches the executor's HTTP runner, or what happens to a failed request.
  • No automatic retry. The background scheduler says that uncertain effects "require reconciliation, never an automatic retry" (crates/service/data-engine-service/src/app_background.rs:1-2). Retry is recorded state (retrying) that an operator or external worker acts on.
  • No arithmetic in action flows, and server time only on create.entity_operation_now_ms_field is accepted only for create (crates/shared/schema/app-manifest/src/validate/action.rs:266-280), so updated_at_ms is the time the delivery was queued and never changes. Bindings copy values without computing them, so attempts cannot be incremented. It stays at the value it was created with (0).
  • One parameter per pipeline invocation. Matching filters on event type only, and the rule's source is a label. A match on (source, event_type) needs two filter bindings or a join against the rule, and the overlay supports one binding.
  • Pipeline results are not live collections. Matches and deliveries are flat result rows in local state. Optimistic projection needs a grouped, hydrated collection, so every delivery change chains a delivery-list run to show the new state.
  • Enablement is display-only. A paused rule hides Queue delivery through v-if. Nothing on the server stops a write that references a paused rule.
  • Host tests need a remount. Rows written into local state by an action do not re-render an already-mounted page in the test harness, so webhookRelay.test.ts remounts the page after each pipeline run, as metricsNotebook.test.ts does.
  • Badge labels need a binding. label="{{ $item.status }}" renders the literal text; :label="$item.status" binds the value.
Copyright © 2026