Skip to content

Handlers

Handlers are the primary way Skein programs receive external input. They declare how a module responds to HTTP requests, queue messages, and scheduled events. Each handler type requires a corresponding capability declaration.

Skein supports four handler types:

TypeCapabilityTriggerExample
HTTPhttp.inIncoming HTTP requesthandler http GET "/users" (req) -> { ... }
Queuequeue.consumeMessage from a named queuehandler queue "events" (msg) -> { ... }
Topictopic.consumeBroadcast message from a named topichandler topic "order.events" (msg) -> { ... }
Scheduleschedule.triggerCron-triggered timerhandler schedule "*/5 * * * *" () -> { ... }

HTTP handlers respond to incoming HTTP requests. They specify an HTTP method, a route pattern, and receive a request map.

handler http METHOD "route" (param) -> {
-- body
}
  • METHOD: One of GET, POST, PUT, PATCH, DELETE
  • route: A string path, optionally with :param segments for path parameters
  • param: The name bound to the request map

The handler parameter receives a map with:

FieldTypeDescription
paramsMapPath parameters extracted from the route
headersMapRequest headers
bodyStringRaw request body
methodAtomHTTP method (:get, :post, etc.)
pathStringRequest path
module UserService {
capability http.in
handler http GET "/users/:id" (req) -> {
let id = req.params.id
respond.json(200, id)
}
handler http POST "/users" (req) -> {
respond.json(201, "created")
}
}

Use req.json[T] to parse and validate JSON request bodies against a declared type:

module OrderService {
capability http.in
type CreateOrder {
product_id: String
quantity: Int @min(1)
}
handler http POST "/orders" (req) -> {
let order = req.json[CreateOrder]
respond.json(201, "ok")
}
}

Queue handlers process messages from named message queues. They are triggered asynchronously when a message is published to the subscribed queue.

handler queue "queue-name" (param) -> {
-- body
}
  • queue-name: A string identifying the queue to subscribe to
  • param: The name bound to the message map
module OrderWorker {
capability queue.consume
handler queue "order-events" (msg) -> {
let data = msg
respond.json(200, "processed")
}
handler queue "priority-events" (msg) -> {
respond.json(200, "priority-processed")
}
}

At startup, queue handlers are registered with Skein.Runtime.Queue. Messages published to the queue are dispatched to the handler function asynchronously. Messages are delivered in order within a single queue.

Schedule handlers run on a cron-based schedule. They take no input parameters and execute periodically.

handler schedule "cron-expression" () -> {
-- body
}
  • cron-expression: A standard 5-field cron expression (minute hour day month weekday)
ExpressionMeaning
*/5 * * * *Every 5 minutes
0 * * * *Every hour on the hour
0 0 * * *Daily at midnight
0 9 * * 1Every Monday at 9:00 AM
30 8 1 * *8:30 AM on the 1st of each month
module Maintenance {
capability schedule.trigger
handler schedule "*/5 * * * *" () -> {
respond.json(200, "cleanup")
}
handler schedule "0 0 * * *" () -> {
respond.json(200, "daily-report")
}
}

At startup, schedule handlers are registered with Skein.Runtime.Schedule. The runtime triggers handlers at the appropriate times. For testing, handlers can be triggered manually with Skein.Runtime.Schedule.trigger/1.

Topic handlers subscribe to named topics and receive every message published to that topic. Unlike queue handlers (which deliver each message to a single consumer), topic handlers use fan-out semantics: every subscriber receives every message.

handler topic "topic-name" (param) -> {
-- body
}
  • topic-name: A string identifier for the topic (e.g., "order.events")
  • param: The message parameter name, bound to the published data
module PubsubNotifications {
capability http.in
capability topic.consume("order.events")
capability topic.publish("order.events")
-- Publish side: an HTTP endpoint fans an event out to every consumer
handler http POST "/orders" (req) -> {
topic.publish("order.events", { order_id: "o-1" })
respond.json(202, "queued")
}
-- Email notification: receives every order event
handler topic "order.events" (msg) -> {
let event = msg
respond.json(200, "email-sent")
}
-- Analytics: also receives every order event (fan-out)
handler topic "order.events" (msg) -> {
let event = msg
respond.json(200, "analytics-recorded")
}
}
QueueTopic
DeliverySingle consumerAll subscribers (fan-out)
Capabilityqueue.consumetopic.consume
Use caseJob processingNotifications, events
Publishqueue.publish(name, data)topic.publish(name, data)

At startup, topic handlers are registered with Skein.Runtime.Topic. Messages published via topic.publish(name, data) are dispatched asynchronously to ALL subscribers. Messages are delivered in order within each subscriber.

A module can declare handlers of multiple types. Each type requires its own capability:

module QueueWorker {
capability http.in
capability queue.consume
capability schedule.trigger
handler http GET "/health" (req) -> {
respond.json(200, "ok")
}
handler queue "jobs" (msg) -> {
respond.json(200, "processed")
}
handler schedule "*/5 * * * *" () -> {
respond.json(200, "cleanup")
}
}

All handler types compile to the same pattern:

  1. Each handler becomes a __handler_N__/1 function in the compiled module
  2. Handler metadata is returned by __handlers__/0 with fields: source, method, route, handler
  3. The source field distinguishes handler types: :http, :queue, :topic, :schedule
# Example __handlers__/0 output for mixed types
[
%{source: :http, method: :get, route: "/health", handler: :__handler_0__},
%{source: :queue, method: nil, route: "jobs", handler: :__handler_1__},
%{source: :schedule, method: nil, route: "*/5 * * * *", handler: :__handler_2__}
]

Handlers return responses using one of three built-in helpers. Each sets the appropriate HTTP Content-Type header automatically.

HelperContent-TypeUse When
respond.json(status, body)application/jsonReturning structured data (maps, lists, values)
respond.text(status, body)text/plainReturning plain text (health checks, simple strings)
respond.html(status, body)text/htmlReturning HTML pages or fragments
module HelloHttp {
capability http.in
-- JSON API endpoint
handler http GET "/api/users" (req) -> {
respond.json(200, "users")
}
-- Plain text health check
handler http GET "/health" (req) -> {
respond.text(200, "ok")
}
-- HTML page
handler http GET "/page" (req) -> {
respond.html(200, "<h1>Hello from Skein</h1>")
}
}

All three helpers take two arguments:

  • status: An integer HTTP status code (e.g., 200, 201, 404, 500)
  • body: A string value to send as the response body

For respond.json, the body is JSON-encoded before sending. For respond.text and respond.html, the body string is sent as-is.

The idempotent(key) guard prevents duplicate processing of messages. Place it at the top of a handler body — if the key has already been processed, the handler skips execution silently.

module BillingWorker {
capability queue.consume
handler queue "billing.events" (msg) -> {
idempotent(msg.id)
-- This code only runs once per unique msg.id
let amount = msg.body
respond.json(200, amount)
}
}

How it works:

  1. On first call with a given key, idempotent() records the key and continues execution
  2. On subsequent calls with the same key, the handler is skipped entirely
  3. Keys expire after a configurable TTL (default: 1 hour), allowing reprocessing after expiry

Constraints:

  • idempotent() can only be used inside handler bodies (not in regular functions or agent handlers). Using it elsewhere produces error E0035.
  • The key argument must be a String expression.

Dispatch behavior on skip:

Handler TypeSkip Response
HTTPReturns 200 "already processed"
QueueSilently drops the message
TopicSilently drops the message
ScheduleSilently skips the invocation

The analyzer checks that each handler type has its required capability declared. Missing capabilities produce error E0012:

Error E0012: Capability 'queue.consume' required but not declared.
Queue handlers require this capability.
Fix: capability queue.consume