Light Dark

Events and Event Handlers

Events are the primary way to trigger asynchronous work in Hot. Event handlers run when specific events occur, enabling decoupled, scalable workflows.

Event Handlers

Define event handlers using the on-event metadata:

::myapp::users ns

// Handle user creation events
on-user-created meta {on-event: "user:created"}
fn (event) {
  // event.data contains the event payload
  send-welcome-email(event.data.email)
  create-default-settings(event.data.id)
}

Event Structure

Events have a standard structure:

FieldDescription
event.typeEvent type, e.g. "user:created"
event.dataThe event payload
event.idUnique event ID
event.timestampWhen the event was created
event.sourceWhere the event came from

Sending Events

Send events from your code to trigger handlers:

// Send an event (send is a core function)
send("user:created", {
  id: user-id,
  email: email,
  name: name
})

Or send events via the REST API:

curl -X POST https://api.hot.dev/v1/events \
  -H "Authorization: Bearer $HOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "user:created", "data": {"id": "123", "email": "user@example.com"}}'

Background Jobs

Any function can be executed as a background job by sending a hot:call event:

# Execute a function asynchronously via event
curl -X POST https://api.hot.dev/v1/events \
  -H "Authorization: Bearer $HOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "hot:call",
    "data": {
      "fn": "::myapp::jobs/process-order",
      "args": [{"order_id": "12345"}]
    }
  }'

Or from Hot code:

// Queue a background job
send("hot:call", {
  fn: "::myapp::jobs/process-order",
  args: [{order_id: "12345"}]
})

The response includes an event_id. You can retrieve runs triggered by the event:

curl https://api.hot.dev/v1/events/$EVENT_ID/runs \
  -H "Authorization: Bearer $HOT_API_KEY"

Retries

Event handlers can automatically retry on failure using the retry metadata:

::myapp::users ns

// Simple format: just retry attempts (uses default 1 second delay)
on-order-completed meta {
  on-event: "order:completed",
  retry: 5
}
fn (event) {
  deliver-webhook(event.data)
}

// Full format: custom retry attempts and delay
on-payment-failed meta {
  on-event: "payment:failed",
  retry: {
    attempts: 3,
    delay: 10000
  }
}
fn (event) {
  notify-payment-failure(event.data)
}

// Retry with default 1 second delay
on-payment-received meta {
  on-event: "payment:received",
  retry: 3
}
fn (event) {
  update-account-balance(event.data)
}

Retry Metadata

The retry metadata supports two formats:

Simple format - just the number of attempts:

retry: 3

Full format - object with attempts and delay:

retry: {
  attempts: 3,
  delay: 5000
}
FieldDescriptionDefault
attempts (or simple number)Maximum retry attempts (1-10)0 (no retries)
delayDelay between retries in milliseconds (max 1 hour)1000 (1 second)

How Retries Work

  1. When an event handler fails (returns a Failure), the system checks for retry configuration
  2. If retries remain, the run is marked as pending_retry with a scheduled retry time
  3. The scheduler picks up pending retries and creates a new run for each retry attempt
  4. Each retry is a separate run linked to the original via origin_run_id
  5. Retries continue until the handler succeeds or max retries are exhausted

Retry runs are marked in the Hot App with a badge showing the attempt number (e.g., "↻1", "↻2").

See Schedule Retries for more details on retry configuration and limits.