Recipes
Recipes are adapters as config. Each recipe is a small YAML file pairing a
destination URL (plus headers) with a transform_template — a Go template
that Sparrow renders server-side, per delivery, turning the generic event
envelope into the destination’s native payload. You get Sparrow’s retries,
signing, and delivery tracking for free; no glue service to run.
Apply one from the dashboard — the webhook registration page has a Start from a recipe picker that prompts for the recipe’s params and pre-fills the destination, headers, and transform template — or with the CLI:
sparrow use slack \ --param webhook_url=https://hooks.slack.com/services/T000/B000/XXX \ --event order.created --event order.refunded \ --label env=prod--event (repeatable) picks which event types the recipe receives; --label k=v adds label filters. Both are apply-time choices, never baked into the
recipe. --param supplies the values the recipe declares.
All shipped recipes are built into the sparrow binary — sparrow recipes
lists them; no files needed. --file (or a ./recipes/<name>.yaml /
$SPARROW_RECIPES_DIR copy) applies your own or overrides a built-in.
How it’s connected
Section titled “How it’s connected”A recipe never runs anywhere. Applying one is pure API configuration, and the rendering happens inside the Sparrow server on every delivery:
apply time (sparrow use <name>): 1. load the recipe YAML (built-in name or --file), prompt for params 2. POST /v1/consumers/{consumer}/webhooks url + headers from the recipe, params substituted 3. PATCH /v1/consumers/{consumer}/subscriptions/{id} transform_enabled: true, transform_template: <recipe template>, events + label filters from --event/--label
delivery time (inside the server, per delivery): event matches subscription → webhook worker renders transform_template against the event (event_id, event_name, timestamp, attempt, payload) → rendered output replaces the envelope as the HTTP body → signed, delivered, retried, and audited like any other webhookTwo consequences worth knowing:
- Retries re-render. A failed delivery retries with the same template;
{{.attempt}}reflects the retry count (PagerDuty’s recipe uses the stable{{.event_id}}asdedup_keyso retries never open duplicate incidents). - Template errors are visible. If rendering fails at delivery time, the
delivery fails with error category
template_errorand nothing is sent (seton_transform_error: fallbackon the subscription to send the plain envelope instead). A field the payload lacks counts as an error, so the included recipes read optional fields withdigorindex. Iterate withsparrow template test, which renders the same way, before applying. See When a template fails.
Included recipes
Section titled “Included recipes”Posts a Block Kit message to a Slack incoming webhook. Param: webhook_url.
Delivered body:
{ "blocks": [ {"type": "header", "text": {"type": "plain_text", "text": "order.created", "emoji": true}}, {"type": "section", "fields": [ {"type": "mrkdwn", "text": "*Event ID:*\nevt_0195c2a1"}, {"type": "mrkdwn", "text": "*Timestamp:*\n2026-01-02T03:04:05Z"}, {"type": "mrkdwn", "text": "*Attempt:*\n1"} ]}, {"type": "section", "text": {"type": "mrkdwn", "text": "```{\"amount\":1999,\"order_id\":\"ord_42\"}```"}} ]}discord
Section titled “discord”Posts an embed to a Discord channel webhook. Param: webhook_url.
{ "embeds": [ { "title": "order.created", "description": "```json\n{\"amount\":1999,\"order_id\":\"ord_42\"}\n```", "timestamp": "2026-01-02T03:04:05Z" } ]}Pushes a notification to an ntfy topic via the JSON publishing endpoint
(POST to the server root). Params: server_url (default https://ntfy.sh),
topic. Publishing as JSON — instead of the header-based endpoint — lets the
title be rendered per delivery from the event name:
{ "topic": "sparrow_alerts", "title": "order.created", "message": "event evt_0195c2a1, attempt 1 at 2026-01-02T03:04:05Z\n\n{\"amount\":1999,\"order_id\":\"ord_42\"}", "tags": ["incoming_envelope"]}pagerduty
Section titled “pagerduty”Triggers an alert via the PagerDuty Events API v2 (/v2/enqueue). Params:
routing_key, severity (fallback, default error — a severity field in
the event payload wins). The event id becomes the dedup_key, so Sparrow’s
retries never open duplicate incidents:
{ "routing_key": "R0UT1NGKEY...", "event_action": "trigger", "dedup_key": "evt_0195c2a1", "payload": { "summary": "Sparrow event: order.created", "source": "sparrow", "severity": "error", "timestamp": "2026-01-02T03:04:05Z", "custom_details": {"amount": 1999, "order_id": "ord_42"} }}sendgrid
Section titled “sendgrid”Sends an email via SendGrid’s v3 Mail Send API. Params: api_key,
from_email, from_name, default_recipient. Built for Sparrow’s own
webhook health alerts: the
transform ranges payload.alert_recipients into one personalizations entry
per recipient, so a single delivery emails everyone opted in. Events without
alert_recipients (custom events) go to default_recipient with a generic
subject naming the event:
{ "personalizations": [ {"to": [{"email": "ops@acme.example.com"}]} ], "from": {"email": "alerts@yourdomain.com", "name": "Sparrow Alerts"}, "subject": "Webhook health: degraded", "content": [{"type": "text/plain", "value": "..."}]}clickhouse
Section titled “clickhouse”Inserts each delivery as a row via ClickHouse’s HTTP interface
(INSERT ... FORMAT JSONEachRow). Params: base_url, table, user,
password (user is sent as the X-ClickHouse-User header; password as an
envelope-encrypted X-ClickHouse-Key secret header). The
target table needs event_id/event_name/timestamp/payload String
columns:
{"event_id": "evt_0195c2a1", "event_name": "order.created", "timestamp": "2026-01-02T03:04:05Z", "payload": "{\"amount\":1999,\"order_id\":\"ord_42\"}"}twilio
Section titled “twilio”Sends each delivery as an SMS via the Twilio Messages API. Params:
account_sid, basic_auth, from_number, to_number. Twilio uses HTTP Basic
auth: basic_auth is base64("<AccountSID>:<AuthToken>"), stored as an
envelope-encrypted Authorization secret header so the auth token is never
persisted in plaintext. The body is application/x-www-form-urlencoded:
To=%2B15559876543&From=%2B15551230000&Body=Sparrow+order.created+%28event+evt_0195c2a1%2C+attempt+1%29+at+2026-01-02T03%3A04%3A05ZWriting your own
Section titled “Writing your own”A recipe is one file, satellites/recipes/<name>.yaml, schema version 1:
version: 1name: myservice # must match the filenamedescription: One-line human descriptionparams: # values supplied at apply time via --param - name: api_token prompt: "MyService API token" required: true - name: api_url prompt: "MyService ingest URL" required: truewebhook: url: '{{param "api_url"}}' headers: {Content-Type: application/json} secret_headers: # envelope-encrypted at rest, masked on read Authorization: 'Bearer {{param "api_token"}}'subscription: transform_template: | {"title": {{.event_name | json}}, "body": {{.payload | json}}}{{param "x"}} tokens in webhook.url, header values, secret_headers values,
and the template are
replaced by the CLI at apply time via plain string substitution. The
substituted template is registered on the subscription and rendered per
delivery with .event_id, .event_name, .timestamp (RFC3339), .attempt,
and .payload. Helpers like json, printf, upper, and ellipsis are
available — see template functions.
Three rules keep templates robust:
- Quote every interpolated value through
json(e.g.{{.event_name | json}}) so quotes or newlines in payloads can’t corrupt the output. - Read fields that may be absent with
digorindex({{ dig "severity" "error" .payload }}): a missing field fails the render. - Iterate locally with
sparrow template test, which renders the template entirely client-side (no API call) against a synthetic sample context, without creating anything.
Real-World Use Cases
Section titled “Real-World Use Cases”Recipes turn Sparrow into a zero-code integration hub. Instead of building, deploying, and maintaining microservice proxies to adapt webhooks for third-party platforms, you declare the target and transform in a single recipe.
Scenario 1: Real-Time Incident Alerting with PagerDuty
Section titled “Scenario 1: Real-Time Incident Alerting with PagerDuty”Goal: Page the on-call engineer immediately whenever a critical system error (system.database_down or payment.gateway_error) occurs in production.
sparrow use pagerduty \ --param routing_key=pd-key-prod-12345 \ --event system.database_down \ --event payment.gateway_error \ --label env=productionWhy this works in real scenarios:
- Zero Alert Noise on Retries: PagerDuty’s recipe uses
.event_idas thededup_key. If the initial HTTP call experiences network jitter, Sparrow’s exponential retries hit PagerDuty’s API without opening duplicate incident tickets. - Selective Targeting: Using
--label env=productionguarantees test or staging failures never wake up on-call engineers.
Scenario 2: Operations & Release ChatOps with Slack & Discord
Section titled “Scenario 2: Operations & Release ChatOps with Slack & Discord”Goal: Give engineering and product teams real-time visibility into customer onboarding and feature deployments directly in team chat channels.
# Post rich Block Kit cards to Slack #signupssparrow use slack \ --param webhook_url=https://hooks.slack.com/services/T00/B00/X123 \ --event user.signup \ --event organization.upgraded
# Post embeds to Discord #release-logsparrow use discord \ --param webhook_url=https://discord.com/api/webhooks/123/abc \ --event deployment.completedWhy this helps: Slack and Discord require strict JSON structures (Block Kit arrays or embed cards). Sparrow renders these templates server-side with zero per-delivery JS VM allocation, delivering formatted cards in milliseconds.
Scenario 3: High-Throughput Event Archiving with ClickHouse
Section titled “Scenario 3: High-Throughput Event Archiving with ClickHouse”Goal: Maintain a real-time append-only event ledger in ClickHouse for analytics, funnels, and fraud detection.
sparrow use clickhouse \ --param base_url=http://clickhouse.internal:8123 \ --param table=raw_webhook_events \ --param user=ingest_user \ --param password=secure_pass \ --event order.created \ --event payment.refundedWhy this helps: ClickHouse ingests rows via JSONEachRow HTTP POST requests. Sparrow’s ClickHouse recipe formats each event into a valid single-line JSON string containing event_id, event_name, timestamp, and stringified payload. Sparrow retries failed database writes with exponential backoff if ClickHouse undergoes maintenance.