name: hogsend-webhooks-and-workflows description: Use when adding an inbound webhook source in src/webhook-sources/ (defineWebhookSource — auth as a match|signature discriminated union, optional Zod schema, transform(payload, ctx) -> IngestEvent | null, served at POST /v1/webhooks/:id), reaching for a built-in integration preset (Clerk/Supabase/Stripe/Segment), or a custom Hatchet task in src/workflows/ passed as extraWorkflows (NOT workflows) to createWorker, including the idempotent batched expand→migrate→contract backfill pattern. Outbound signed webhooks are managed separately (hogsend webhooks CLI / hs.webhooks). license: MIT metadata: author: withSeismic version: "1.0.0"
Hogsend webhooks & workflows
This skill covers the two extension points a scaffolded Hogsend app uses to take in external events and run background jobs:
- Webhook sources — turn an inbound HTTP payload into an
IngestEventthat flows through the engine's ingestion pipeline (and can trigger journeys). - Custom Hatchet tasks — durable background work (one-off jobs, backfills, cron-style maintenance) registered alongside the engine's built-in workflows.
You are editing a content-only consumer: you import everything from
@hogsend/engine (and @hogsend/db for tasks). Never edit engine internals.
Relative imports use the ESM .js extension.
Capability map / key concepts
defineWebhookSource({ meta, auth, schema?, transform })(from@hogsend/engine) — declares one source served atPOST /v1/webhooks/:id.authis a discriminated union ontype:"match"(shared-secret equality against a header/Authorization: Bearer; OPEN when the secret is unset) or"signature"(scheme: "svix" | "stripe" | "hmac-hex", with anenvKey, optionalheader/fallbackMatchHeader; FAILS CLOSED with 401 when the secret is unset).schemais an optional Zod validator;transform(payload, ctx)returns anIngestEvent | null(null= accept-and-skip). Register sources insrc/webhook-sources/index.tsand pass them tocreateApp(client, { webhookSources })insrc/index.ts.- Built-in integration presets — the engine ships four ready-made inbound
sources (Clerk, Supabase, Stripe, Segment) served at
POST /v1/webhooks/{clerk,supabase,stripe,segment}with no code to write. Each mounts only when its secret env var is set ANDENABLED_WEBHOOK_PRESETSallows it ("*"/absent = auto, a csv of ids = exactly those,"none"= off). Defining your own source with the SAME id overrides the preset (you win). IngestEvent— the shapetransformmust return:{ event, userId, userEmail, properties, idempotencyKey? }. The route feeds it straight intoingestEvent(), so a webhook can enroll users into journeys.- Custom Hatchet tasks — define with
hatchet.task({ name, fn })(orhatchet.durableTaskfor event-driven/long-running work), export fromsrc/workflows/index.tsin theextraWorkflowsarray, and pass it ascreateWorker({ ..., extraWorkflows })— the option isextraWorkflows, NOTworkflows. Never list the engine's built-ins (send-email, import-contacts, check-alerts, bucket tasks) — those register automatically. - JSON-serializable IO — task input AND return value must serialize to JSON.
Use specific keys or
JsonValue-compatible types; do not use a[key: string]: unknownindex signature. - Backfill pattern —
runBatchedBackfill()(from@hogsend/engine) drives a long data migration in small, idempotent, lock-friendly batches from inside a task — the supported home for bulk data changes (never inside a schema migration). Follow expand → migrate → contract across releases.
Task playbooks — load the matching reference
- Adding / editing an inbound webhook source → load
references/webhook-source.md(defineWebhookSource fields, thetransform→ingestEventcontract, auth matching, registration +createAppwiring). - Writing a custom Hatchet task (one-off job, cron, event-driven) → load
references/custom-workflow.md(hatchet.task/durableTask, JSON-serializable IO, export fromindex.ts,createWorker({ extraWorkflows })). - Backfilling a new column on existing rows → load
references/backfill-pattern.md(the idempotent batched expand→migrate→contract job from the template example).
Cross-skill pointers
- A webhook's
transformonly needs to emit the rightevent/properties; whether a journey then enrolls or exits is decided by trigger/exit conditions — see the hogsend-conditions skill forwhere/exitOn/criteria and duration helpers. - To verify a webhook or task against a running instance (events landing, contacts upserted, journeys firing), see the hogsend-cli skill.
- Inbound vs outbound: this skill is about inbound sources (HTTP → engine).
The engine also emits an outbound event stream (
contact.*,email.*,journey.completed,bucket.*). Two halves:- Subscriber endpoints — manage the
webhook_endpointsrows withhogsend webhooks …(hogsend-cli skill) orhs.webhooks.*(hogsend-client-sdk skill); verify signed deliveries withverifyHogsendWebhook.create/updatenow take akind+config: the defaultkind="webhook"is the byte-identical signedwhsec_POST (returns a one-timesecret); a keyed destination (kind="posthog"|"segment"|"slack"|…) carries its credentials inconfig(e.g.{ apiKey }) and returns NO secret. Same durable retry/backoff/DLQ spine either way —kindjust selects the delivery-time transform. - Code-defined destinations — author a delivery-time transform for a new
fan-out TARGET (a CRM, a warehouse, a custom shape) with
defineDestination()insrc/destinations/. This is the symmetric twin ofdefineWebhookSourceon the OUTBOUND side. → the hogsend-authoring-destinations skill. NOTE: outbound is no longer "just code in a journey" — for event fan-out, reach for a destination, not a per-step integration call.
- Subscriber endpoints — manage the