Dispatch

Relay Hook

Emit typed webhook events at every Dispatch lifecycle point using relay_hook.

The relay_hook package bridges Dispatch lifecycle events to Relay for webhook delivery. When registered as a Dispatch extension, it emits a typed webhook event at every job, workflow, cron, and shutdown lifecycle point.

Setup

import (
    "github.com/xraph/relay"
    "github.com/xraph/dispatch/relay_hook"
    "github.com/xraph/relay/store/memory"
)

// Create a Relay instance.
r, _ := relay.New(relay.WithStore(memory.New()))

// Register the event types Dispatch will emit.
relay_hook.RegisterAll(ctx, r)

// Create the hook extension.
hook := relay_hook.New(r)

// Register with the engine.
eng := engine.Build(d,
    engine.WithExtension(hook),
)

Emitted event types

Event typeLifecycle point
dispatch.job.enqueuedJob accepted into queue
dispatch.job.startedWorker begins execution
dispatch.job.completedJob finishes successfully
dispatch.job.failedJob fails terminally
dispatch.job.retryingJob fails, scheduled for retry
dispatch.job.dlqJob moved to DLQ
dispatch.workflow.startedWorkflow run begins
dispatch.workflow.step.completedWorkflow step completes
dispatch.workflow.step.failedWorkflow step fails
dispatch.workflow.completedWorkflow run completes
dispatch.workflow.failedWorkflow run fails
dispatch.cron.firedCron entry fires
dispatch.shutdownDispatcher shuts down

Filtering events

Restrict which events are emitted:

hook := relay_hook.New(r,
    relay_hook.WithEvents(
        relay_hook.EventJobCompleted,
        relay_hook.EventJobFailed,
        relay_hook.EventJobDLQ,
        relay_hook.EventWorkflowFailed,
    ),
)

Event payload

Each webhook event carries a JSON payload with the full entity at the time of the lifecycle event. For example, dispatch.job.completed includes:

{
  "job_id": "job_01h2xcejqtf2nbrexx3vqjhp41",
  "name": "send_email",
  "queue": "default",
  "elapsed_ms": 142,
  "completed_at": "2025-01-15T09:00:00Z"
}

Tenant routing

Webhook events are routed to Relay endpoints matching the job's ScopeOrgID as the TenantID. If ScopeOrgID is empty, events are routed to a global tenant.

On this page