Light Dark

Metadata

Metadata in Hot uses the meta keyword to attach information to functions, types, and namespaces. This powers documentation, testing, event handling, scheduling, and more.

Syntax

Metadata comes in two forms:

Map Form

Use meta {...} for key-value metadata:

greet-meta
meta {doc: "Greets a user by name"}
fn (name: Str): Str {
  `Hello, ${name}!`
}

Multiple fields:

process-meta
meta {
    doc: "Process incoming data",
    core: true
}
fn (data: Any): Any {
  data
}

Vector Form

Use meta [...] for simple tags:

test-greet-demo
meta ["test"]
fn () {
  assert-eq(greet-meta("World"), "Hello, World!")
}

Documentation

The doc field provides documentation for functions and types:

add-demo
meta {doc: "Add two numbers together"}
fn (a: Int, b: Int): Int {
  ::hot::math/add(a, b)
}

User
meta {doc: "Represents a user in the system"}
type {
  name: Str,
  email: Str
}

Documentation is displayed in the Hot App dashboard and used by tooling.

Core Functions

The core: true metadata marks functions and types as globally available without namespace qualification:

// In ::hot::math namespace
add
meta {core: true, doc: "Add two numbers"}
fn (a: Int, b: Int): Int {
  // ...
}

Now add can be called from any namespace without the ::hot::math/ prefix:

::myapp::calculator ns

// No need for ::hot::math/add
result add(1, 2)

Making Your Functions Core

You can mark your own functions as core too:

::myapp::utils ns

// This function will be available everywhere in your project
format-currency
meta {core: true, doc: "Format a number as currency"}
fn (amount: Dec): Str {
  `$${amount}`
}
::myapp::orders ns

// Use without namespace prefix
total format-currency(99.99)

This is useful for utility functions you use throughout your codebase.

Test Functions

Mark functions as tests with meta ["test"]:

test-add-demo
meta ["test"]
fn () {
  assert-eq(add-demo(1, 2), 3)
}

test-greet-check
meta ["test"]
fn () {
  result greet-meta("World")
  assert(starts-with(result, "Hello"))
}

Run tests with hot test.

Event Handlers

The on-event field registers a function as an event handler:

send-welcome-email
meta {on-event: "user:created"}
fn (event) {
  ::email/send({
    to: event.data.email,
    subject: "Welcome!",
    body: `Welcome, ${event.data.name}!`
  })
}

When a user:created event is sent, this handler runs automatically.

Scheduled Functions

The schedule field runs functions on a schedule:

cleanup-old-sessions
meta {schedule: "@daily"}
fn (event) {
  ::db/delete-expired-sessions()
}

send-heartbeat
meta {schedule: "every 30 seconds"}
fn (event) {
  ::monitoring/ping()
}

generate-report
meta {schedule: "every 1 hour"}
fn (event) {
  ::reports/generate-hourly()
}

Schedule formats:

  • "@daily", "@hourly", "@weekly"
  • "every N seconds", "every N minutes", "every N hours"

MCP Tools

The mcp field exposes a function as a Model Context Protocol tool, making it callable by AI models and agents:

get-weather
meta {
  mcp: {
    service: "weather",
    description: "Get current weather for a city"
  }
}
fn (city: Str): Map {
  ::http/get(`https://api.weather.com/current?city=${city}`).body
}

The mcp value is a map with these fields:

FieldRequiredDescription
serviceYesGroups tools into a named service with its own endpoint
authNo"required" (default) or "none". Controls whether Hot validates credentials before invocation.
nameNoOverride the auto-generated tool name
descriptionNoHuman-readable description (helps AI choose the right tool)
titleNoDisplay title
input-schemaNoOverride auto-generated input JSON Schema
output-schemaNoOverride auto-generated output JSON Schema
annotationsNoMCP behavioral hints (readOnlyHint, destructiveHint, etc.)

Input and output schemas are automatically generated from the function's type signature. Tools are grouped by service and accessible via the MCP endpoint at /mcp/{org}/{env}/{service}.

See MCP Services for the full reference on services, schemas, endpoints, and best practices.

Webhook Endpoints

The webhook field exposes a function as a webhook endpoint, allowing external services to send HTTP requests to your Hot functions:

on-slack-event
meta {
  webhook: {
    service: "slack",
    path: "/events",
    description: "Handle incoming Slack events"
  }
}
fn (request: HttpRequest): HttpResponse {
  event from-json(request.body-raw)
  handle-event(event)
  HttpResponse({status: 200, body: {ok: true}})
}

The webhook value is a map with these fields:

FieldRequiredDescription
serviceYesGroups endpoints into a named service (part of the URL)
pathYesURL path within the service (e.g., /events)
methodNoHTTP method to match (default: POST)
nameNoOverride the auto-generated endpoint name
descriptionNoHuman-readable description
authNo"none" (default, public) or "required" (requires Bearer token — API key, service key, or session)

Webhook endpoints are public by default and receive an HttpRequest with the full HTTP request details (from ::hot::http). Return an HttpResponse to control the status code, headers, and body—all fields except status are optional.

See Webhooks for the full reference on authentication, signature verification, and best practices.

Secret Headers

The secret-headers field is a top-level meta field (not nested under mcp or webhook) that declares additional HTTP header names whose values should be masked in run logs. It works for both MCP tools and webhook handlers:

list-invoices
meta {
  mcp: {service: "billing", auth: "none"},
  secret-headers: ["x-api-key"]
}
fn (status: Str?): Vec { ... }

stripe-payment
meta {
  webhook: {service: "stripe", path: "/payment"},
  secret-headers: ["stripe-signature"]
}
fn (request: HttpRequest): HttpResponse { ... }

The following headers are always masked automatically: authorization, cookie, proxy-authorization, set-cookie. The entire auth subtree (in hot.request and in the webhook HttpRequest argument) is also always masked. Use secret-headers for custom credential headers specific to your integration.

Retry Configuration

Event handlers and scheduled functions can automatically retry on failure using the retry field:

Simple Format

Just specify the number of retry attempts (uses default 1 second delay):

process-payment
meta {on-event: "payment:process", retry: 3}
fn (event) {
  // Will retry up to 3 times on failure
  charge-card(event.data)
}

Full Format

Specify custom attempts, delay, and advanced options:

sync-external-data
meta {
  schedule: "@hourly",
  retry: {
    attempts: 5,
    delay: 10000,
    backoff: "exponential",
    max_delay: 300000,
    jitter: true
  }
}
fn (event) {
  // Will retry up to 5 times with exponential backoff
  // Starting at 10s, doubling each attempt, capped at 5 minutes
  fetch-and-sync()
}

See Retries for retry fields, backoff behavior, and platform limits.

Context Requirements

The ctx field declares context variables (secrets and configuration) that a namespace requires:

::myapp::api ns
meta {ctx: {
  "openai.api.key": {required: true},
  "rate.limit": {required: false, default: 1000, secret: false}
}}

Per-Key Properties

PropertyTypeDefaultDescription
requiredbooltrueMust be provided at runtime
defaultanynoneValue if not provided (implies required: false)
secretbooltrueIf true, value will be masked in call logs

Examples

Required secret (most common):

meta {ctx: {"anthropic.api.key": {required: true}}}

Optional with default (non-secret):

meta {ctx: {"rate.limit": {default: 60, secret: false}}}

Multiple keys:

meta {ctx: {
  "aws.access_key_id": {required: true},
  "aws.secret_access_key": {required: true},
  "aws.region": {required: false, default: "us-east-1", secret: false}
}}

Secret Masking

By default, all context values are considered secrets (secret: true). When a function calls ::hot::ctx/get to retrieve a secret, the return value is masked as "<secret>" in the call database to prevent accidental exposure.

Mark a value as secret: false if it's safe to log (like configuration values, rate limits, etc.).

Runtime Functions

Use these functions to access context values at runtime:

// Get a context value
api-key ::hot::ctx/get("openai.api.key")

// Set a context value
::hot::ctx/set("my.config", "value")

// Set a secret value (explicitly marks as secret for masking)
::hot::ctx/set-secret("api.token", token-value)

Namespace Metadata

You can also attach metadata to namespaces:

::myapp::test::users meta ["test"] ns

// All functions in this namespace are test-related

Combining Metadata

Combine multiple metadata fields in one map:

process-order
meta {
  doc: "Process an incoming order",
  on-event: "order:created",
  core: true
}
fn (event) {
  // ...
}

Summary

MetadataPurpose
doc: "..."Documentation
core: trueGlobally available without namespace
meta ["test"]Mark as test function
on-event: "name"Event handler
schedule: "..."Scheduled execution
mcp: {...}Expose as MCP tool
webhook: {...}Expose as webhook endpoint
retry: N or retry: {...}Automatic retry on failure
ctx: {...}Declare required context variables (secrets)