# Hot Dev Documentation > Complete public documentation for the Hot language and platform. This document is generated from the same Markdown sources as hot.dev/docs. Page source URLs are included before each section. --- Source: https://hot.dev/docs # Hot Docs Welcome to the Hot documentation. Hot is a backend workflow automation platform for the AI age. ## Overview - **[Getting Started](/docs/getting-started)** - Install Hot and run your first workflow - **[Demos](/docs/demos)** - Run complete tutorial projects, starting with Hot Chat - **[Hot Language](/docs/language)** - Learn the Hot programming language - **[Events & Handlers](/docs/events)** - Event handlers and background jobs - **[Schedules](/docs/schedules)** - Cron and scheduled runs - **[Agents](/docs/agents)** - Define intelligent agents with memory and tools - **[MCP Services](/docs/mcp)** - Expose Hot functions as MCP tools for AI agents - **[Webhooks](/docs/webhooks)** - Turn Hot functions into webhook endpoints - **[Hot API](/docs/api)** - Integrate Hot with your backend - **[Hot App](/docs/app)** - Monitor and debug executions - **[Hot Packages](/pkg)** - Standard library and integrations - **[Migrations and Upgrades](/docs/migrations)** - Update Hot and move databases between release lines - **[VS Code & LSP](/docs/editor)** - Editor support and tooling ## Getting Help - Email: [support@hot.dev](mailto:support@hot.dev) --- Source: https://hot.dev/docs/getting-started # Getting Started Get up and running with Hot in minutes. ## 1. Install Hot ### macOS / Linux ```bash curl -fsSL https://get.hot.dev/install.sh | sh ``` ### Windows (PowerShell) ```powershell irm https://get.hot.dev/install.ps1 | iex ``` ### Homebrew ```bash brew install hot-dev/hot/hot ``` --- Verify it works: ```bash hot version ``` > **Already have Hot?** Update to the latest version with `hot update`. To install a specific release from an older `hot` binary, use `curl -fsSL https://get.hot.dev/install.sh | sh -s -- --version `. ## 2. Install Editor Extension For syntax highlighting, autocomplete, and diagnostics, install the Hot extension: - **VS Code** — Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=hot-dev.hot), or search for "Hot" by `hot-dev` in the Extensions panel (Cmd+Shift+X / Ctrl+Shift+X) - **Cursor, Windsurf & other VS Code-compatible editors** — Search for "Hot" by `hot-dev` in the Extensions panel ([also on Open VSX](https://open-vsx.org/extension/hot-dev/hot)) Or install from the command line: ```bash code --install-extension hot-dev.hot ``` ## 3. Add AI Coding Support (Optional) If you use an AI coding assistant, add Hot language support to help it understand your code: ```bash hot ai add # Add AGENTS.md + skills to project hot ai add --global # Install skills to ~/.skills/ (available in all projects) ``` This creates an `AGENTS.md` file and installs the skills bundled with your Hot release under `.skills/`. Current bundles include Hot language guidance and may include specialized guidance such as Hot AI agents and SDK integrations. Works with Cursor, Claude Code, GitHub Copilot, Windsurf, and many other AI coding tools. `hot ai add` uses the skill snapshots bundled with your installed Hot release; use `hot ai list` to see that version's catalog. If you prefer the latest public skills through the skills.sh ecosystem, run `npx skills add hot-dev/hot-skills`. See [AI Coding Assistants](/docs/ai-coding) for project and global setup, validation, troubleshooting, and LLM-readable documentation. ## 4. Initialize Hot is designed to live alongside your existing code. Add it to an existing project or start fresh: ```bash # Add Hot to an existing project cd my-app hot init # Or start a new project hot init my-app cd my-app ``` The project name comes from the directory name. `hot init` adds three things to the directory: | What | Where | Purpose | |------|-------|---------| | `hot.hot` | Project root | Configuration file | | `hot/` | `hot/src/`, `hot/test/`, `hot/pkg/` | Your Hot code | | `.hot/` | `.hot/` (gitignored) | Local data — cache, database, logs | That's it. Your existing files are untouched: ``` my-app/ ├── src/ # Your existing code ├── package.json # Your existing config ├── hot.hot # Hot config (project root) ├── hot/ # Hot code goes here │ ├── src/ │ │ └── my-app/ │ │ └── hi.hot # Your first Hot file (it's a tutorial!) │ └── test/ └── .hot/ # Local data — cache, db, logs (gitignored) ``` The project name (`my-app`) becomes your root namespace. All functions live under `::my-app::*`. ## 5. Start Hot Dev ```bash hot dev ``` This starts the development server with: - **App** at [http://localhost:4680](http://localhost:4680) — monitor runs, events, and streams - **Scheduler** — executes your scheduled functions - **Worker** — processes events Open the app and watch things happen! The `hi.hot` file includes a scheduled function that runs every minute. > **Tip:** Use `hot dev --open` to automatically open the dashboard in your browser. You can also set `hot.dev.open true` in your `hot.hot` config to always open on start. ## 6. Run Some Code Open another terminal and try: ```bash # Call a function hot eval '::my-app::hi/hello()' # Call with arguments hot eval '::my-app::hi/check-heat(42)' # This one fails! (7 is divisible by 7) hot eval '::my-app::hi/check-heat(7)' # Trigger an event (hi.hot has a handler for this) hot eval 'send("hot-take", {num: 42})' ``` Check the dashboard to see your runs and events. ## 7. Learn from hi.hot Open `hot/src/my-app/hi.hot` in your editor—it's a complete tutorial covering: - **Functions** — basic definitions, arguments, and types - **Flows** — `cond`, `parallel`, and pipes (`|>`) - **Result handling** — using `match` on `Result.Ok` and `Result.Err` - **Schedules** — functions that run on a timer - **Events** — handlers that respond to events Edit the file and Hot Dev reloads automatically. Experiment! ## Deploy to Hot Cloud When you're ready to go live, deploy to Hot Cloud with a single command. ### Get a Hot API Key 1. Go to [hot.dev](https://hot.dev) and sign in (or create an account) 2. Navigate to **Settings → API Keys** 3. Click **Create API Key** and copy the key 4. Set your API key in your environment: ```bash export HOT_API_KEY=your-api-key ``` Or store it in a `.env` file in your project root. ### Deploy ```bash hot deploy ``` This builds your project and deploys it to Hot Cloud. Your workflows, schedules, and event handlers will now run in production. > **Automate it:** Use the [Hot GitHub Action](/docs/ci-cd) to deploy on every push to `main`. ## Next Steps - **[Hot Chat demo](/docs/demos/hot-chat)** — run a complete AI chat product on Hot in 15 minutes - **[Hot Language](/docs/language)** — dive deeper into syntax and concepts - **[Standard Library](/pkg/hot-std)** — explore available functions - **[Events & Handlers](/docs/events)** — build event-driven workflows - **[MCP Services](/docs/mcp)** — expose functions as tools for AI agents - **[Webhooks](/docs/webhooks)** — receive HTTP requests from external services - **[CI/CD](/docs/ci-cd)** — automate testing and deployment with GitHub Actions --- Source: https://hot.dev/docs/adopting-hot # Adopting Hot in an Existing Application Hot is designed to live beside the application you already have. You do not need to move your API, database, frontend, or every background job at once. Start with one workflow boundary that is difficult to run, retry, or debug today. This guide covers an incremental adoption path for existing JavaScript, Python, Go, Rust, Java, and other applications. ## Choose a Good First Workflow The best first workflow is operationally meaningful but isolated enough to move safely: - A webhook that performs several downstream actions - A cron job that needs history, alerts, or retries - A queue worker whose failures are difficult to diagnose - An AI agent loop that needs durable tools, memory, or streaming - A browser, media, OCR, or data job that needs an isolated container - A multi-step process already connected by application events Avoid starting with the broadest or most latency-sensitive path in your system. The first goal is to evaluate the Hot development and operating model, not prove that every backend concern belongs in Hot. ## 1. Install and Initialize Install Hot, then initialize it in the existing repository: ```bash curl -fsSL https://get.hot.dev/install.sh | sh cd my-existing-app hot init ``` `hot init` adds `hot.hot`, `hot/`, and the gitignored `.hot/` directory. Your existing source and configuration remain in place. Run the local platform: ```bash hot dev --open ``` This starts the API, scheduler, worker, and Hot App. See [Getting Started](/docs/getting-started) for the complete setup path. ## 2. Define the Boundary Treat the event payload or function arguments as a contract between your application and Hot. For example, an existing application can publish a `customer:created` event: ```json { "event_type": "customer:created", "event_data": { "id": "cus_123", "email": "new@example.com", "plan": "starter" } } ``` The first Hot handler can own one downstream action: ```hot ::myapp::customers ns send-welcome-email meta { doc: "Send the first product email to a new customer", on-event: "customer:created", retry: {attempts: 5, delay: 1000, backoff: "exponential"}, } fn (event) { customer event.data deliver-welcome-email(customer.email, customer.plan) } ``` Keep event names and payloads explicit. Add identifiers needed for idempotency, correlation, authorization, and debugging at the boundary rather than fetching them implicitly from unrelated process state. ## 3. Connect the Existing Application Applications can publish events through the Hot HTTP API: ```bash curl -X POST http://localhost:4681/v1/events \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "customer:created", "event_data": { "id": "cus_123", "email": "new@example.com", "plan": "starter" } }' ``` For application code, use an [official Hot SDK](/docs/api/sdks) for JavaScript/TypeScript, Python, Go, Rust, or Java. SDKs can publish events, call Hot functions, and subscribe to streams without hand-building each request. Authenticated Hot clients belong on trusted servers. Do not expose a Hot API key in browser or mobile client code. ## 4. Make Side Effects Idempotent Hot events use at-least-once delivery. A handler may receive the same logical event more than once because of retries or infrastructure redelivery. For side effects such as charging a card, sending an email, or provisioning an account: 1. Put a stable idempotency key in the event payload. 2. Pass it to an external API when that API supports idempotency. 3. Otherwise record completion in the system that owns the side effect. 4. Return the existing result when the same key is seen again. ```hot charge-customer meta { on-event: "billing:charge-requested", retry: 3, } fn (event) { key event.data.idempotency-key existing find-charge(key) if( is-some(existing), existing, create-charge(event.data, key), ) } ``` See [Durable Execution](/docs/platform/durability) for delivery, retries, event lineage, and long-running task behavior. ## 5. Test the Operational Path Exercise success, failure, and retry behavior locally: ```bash hot test hot dev --open ``` For the first migrated workflow, verify: - The source application can publish the event or call the function. - Payload validation fails clearly when required fields are missing. - A transient failure retries with the expected policy. - A duplicate event cannot repeat a protected side effect. - Inputs, results, failures, and intermediate values appear in Hot App. - Downstream events preserve the identifiers needed to follow the chain. - Alerts reach the intended destination when retries are exhausted. ## 6. Cut Over Gradually Choose a rollout method based on the side effect: ### Shadow Publish the event to Hot while the existing worker remains authoritative. Let the Hot handler validate, transform, or calculate without performing the final side effect. Compare results before switching ownership. ### Dual-read Let Hot process the workflow while both the old and new observability paths are available. Keep only one path authorized to perform non-idempotent actions. ### Narrow cutover Move a small cohort, event type, tenant, or scheduled invocation to Hot. Expand after successful runs and failure recovery have been observed. Do not run two independently authorized implementations of a payment, notification, or provisioning side effect unless both share a proven idempotency boundary. ## Common Migration Patterns ### Cron job to schedule Move the job body into a Hot function and add `schedule` metadata: ```hot daily-account-sync meta { schedule: "every day at 2am", retry: {attempts: 3, backoff: "exponential"}, } fn (event) { sync-accounts() } ``` Use Hot App for run history and [Alerts](/docs/alerts) for failure notifications. ### Queue worker to event handler Publish a domain event through the API or an SDK, then attach one or more Hot handlers with `on-event`. Each handler becomes an independently persisted and retryable run. See [Events & Handlers](/docs/events). ### HTTP webhook to Hot webhook Add `webhook` metadata to a function, validate the incoming request, and emit an internal event for downstream work. This keeps the externally visible response path short while durable handlers perform slower side effects. See [Webhooks](/docs/webhooks). ### Long-running worker to task Use a [code task](/docs/tasks) for long-running Hot code with messaging and checkpoints. Use a [container task](/docs/box) when the job needs a browser, system binary, Python environment, media tool, or custom OCI image. ### AI loop to agent Define typed agent identity and attach handlers, schedules, webhooks, tools, and memory patterns. Keep model calls and side effects in observable functions with explicit event or tool boundaries. See [Agents](/docs/agents) and the [Hot Chat demo](/docs/demos/hot-chat). ## When to Move the Next Workflow Expand Hot's boundary when the first workflow demonstrates a clear improvement in at least one of these areas: - Less queue, worker, scheduler, or deployment infrastructure to operate - Faster diagnosis through run, event, and expression traces - Safer recovery through independent retries or task checkpoints - A clearer contract between application code and background work - Reusable tools, packages, events, or workflow patterns - A simpler path from local development to production If the workflow remains simpler and clearer in the existing application, keep it there. Hot should own the work that benefits from its execution and observability model. ## Next Steps - [Getting Started](/docs/getting-started) - [Official SDKs](/docs/api/sdks) - [Events & Handlers](/docs/events) - [Durable Execution](/docs/platform/durability) - [CI/CD](/docs/ci-cd) - [Hot Cloud Pricing](/pricing) --- Source: https://hot.dev/docs/ai-coding # AI Coding Assistants Hot ships coding-agent context because conventional language assumptions do not apply to Hot syntax or execution semantics. Install the bundled instructions before asking an AI assistant to create or change Hot code. This guide is about **coding with AI assistants**. To build an AI agent as part of your product, see [Agents](/docs/agents). ## Install Project Context From the root of a Hot project: ```bash hot ai add ``` This installs: - `AGENTS.md` — concise repository instructions and critical Hot syntax rules - `.skills/hot-language/` — detailed language guidance, references, and examples - `.skills/hot-ai-agents/` — agent, model, tool, memory, and AI SDK integration guidance Project-local context is the best default. It travels with the repository and lets the team review changes to the instructions alongside the code. ## Install Skills Globally To make the bundled Hot skills available across projects: ```bash hot ai add --global ``` Global skills are installed under `~/.skills/`. The exact skill set can vary by Hot release. Inspect what is installed: ```bash hot ai list ``` Refresh installed files after upgrading Hot: ```bash hot ai update ``` `hot ai add` and `hot ai update` use snapshots bundled with the installed Hot release, so they work offline and stay version-aligned with the compiler. ## Install the Latest Public Skills The public skills are also published from [hot-dev/hot-skills](https://github.com/hot-dev/hot-skills): ```bash npx skills add hot-dev/hot-skills ``` Use the bundled `hot ai add` path when release alignment and offline installation matter. Use the public skills source when you intentionally want the newest published guidance. ## Give the Assistant a Clear Task Good coding-agent requests name the workflow behavior, existing namespace, and validation command: ```text Use the Hot language skill for this task. Add an event handler in ::billing that listens for invoice:overdue, posts a Slack notification, and retries transient failures three times. Preserve the existing event payload shape. Run hot check and the relevant Hot tests when finished. ``` For agent or model integrations, also ask the assistant to use the `hot-ai-agents` skill: ```text Use the hot-language and hot-ai-agents skills. Add a support agent with project-specific memory and one permission-scoped MCP tool. Follow the existing namespace and package patterns. Do not invent package APIs; inspect the installed package docs first. ``` The explicit skill request is useful in tools that load skills on demand. `AGENTS.md` remains passive repository context for tools that support the standard. ## Validate Generated Hot Code AI-generated Hot code should be treated like any other code change: ```bash hot check hot test ``` When working inside this repository, use the project-specific commands in its `AGENTS.md`. Some Hot repositories run tests through Cargo: ```bash cargo run test ``` Review generated code for the Hot-specific mistakes that general-purpose models make most often: - Assignment uses `name value`, not `name = value`. - Arithmetic and comparison use functions such as `add` and `eq`, not infix operators. - Conditional flows use `if(...)`, `cond`, or `match`, not conventional `if`/`else` blocks. - Ordinary function bodies are serial; only explicit parallel flows run independent bindings concurrently. - Expected failures use `Result.Err` patterns; `fail(...)` is for broken invariants. - Event handlers with external side effects account for at-least-once delivery. - Namespaced package functions are inspected rather than guessed. The bundled language skill contains the complete rules and examples. ## LLM-Readable Documentation Hot publishes two plain-text documentation resources: - [`https://hot.dev/llms.txt`](https://hot.dev/llms.txt) — compact documentation index with page descriptions - [`https://hot.dev/llms-full.txt`](https://hot.dev/llms-full.txt) — the full user documentation in navigation order Use the compact index for tools that can retrieve individual pages. Use the full document when a tool needs a single context artifact and its context window can hold the content. The website documentation remains the source of truth for current public behavior. Bundled skills focus on the rules and workflows a coding agent needs to make correct changes. ## Keep Secrets Out of Agent Context Do not paste production API keys, model keys, customer data, `.env` contents, or private run payloads into an assistant unless the selected tool and environment are explicitly approved for that data. Hot code reads secrets from context variables: ```hot api-key ::hot::ctx/get("anthropic.api.key") ``` For local development, put the value in `.env`: ```text ANTHROPIC_API_KEY=your-provider-key ``` Then map it to a Hot context key in `hot/ctx.hot`: ```hot ::hot::run::ctx ns ::env ::hot::env ::hot::ctx/set({ "anthropic.api.key": ::env/get("ANTHROPIC_API_KEY", "") }) ``` Deployed projects do not use `hot/ctx.hot`. Set the same context key in **Context Variables** in the Hot App for the target environment or project. Use placeholders—not real values—in prompts and examples. See [Context Variables](/docs/app#context-variables) and [Context Requirements](/docs/language/meta#context-requirements). ## Troubleshooting ### The assistant writes conventional syntax Confirm `AGENTS.md` is visible from the working directory and explicitly ask the assistant to load the `hot-language` skill. ```bash hot ai list ``` ### Instructions are from an older Hot release Upgrade Hot, then refresh installed AI support: ```bash hot update hot ai update ``` ### A package API is invented Direct the assistant to the installed [Hot package docs](/pkg) and ask it to verify the exact namespace, function, type, and context requirements before editing code. ### The skill is unavailable in a specific tool Keep `AGENTS.md` in the repository and provide the relevant page from `llms.txt` or `llms-full.txt` as context. Skill discovery differs between coding tools, while plain repository instructions and Markdown remain widely usable. ## Next Steps - [Getting Started](/docs/getting-started) - [Hot Language](/docs/language) - [Hot CLI](/docs/cli) - [Agents](/docs/agents) - [Hot Packages](/pkg) --- Source: https://hot.dev/docs/demos # Demos Hot demos are complete, runnable projects you can clone, edit, and deploy. Each one teaches one end-to-end pattern. The pages here explain *what to look for* and *how to read* what you see in the Hot App; the source projects live in [hot-dev/hot-demos](https://github.com/hot-dev/hot-demos). ## Start Here: Hot Chat **[Hot Chat](/docs/demos/hot-chat)** is the culmination of several Hot Dev platform features that combine into a powerful solution for AI-driven products — and the best first impression of building on Hot. One Hot project boots two AI agents that the Next.js client switches between live: - **Personal Mode** — identity-first memory. Notes follow the user across sessions and devices. - **Team Mode** — session-first memory. Channel members share one memory; channels stay independent. You get a polished chat UI, file attachments, streaming replies, and a transparent identity panel — all over the same typed-event wire contract a Slack or Telegram adapter would use. Each slash command is one `on-event` handler in the agent, so the Agent Graph stays readable as the agent grows. Run it with two terminals: ```bash git clone https://github.com/hot-dev/hot-demos cd hot-demos/hot-chat hot dev --open # terminal 1 — both agents cp .env.example .env && npm install && npm run dev # terminal 2 — UI ``` ## Identity Vocabulary Both modes work with the same two ideas. Holding them apart makes the demo easier to read. | Concept | Team Mode | Personal Mode | |------------|---------------------------------|----------------------------------------| | Session | the channel or thread | a scratch context per person | | Identity | the person who posted a message | the durable memory owner | | Memory | scoped to the session | scoped to the user | Slack and Telegram adapters fill these in from native IDs. Hot Chat uses synthetic IDs from `localStorage` so you can run everything without an account. ## More Recipes In The Repo These projects live in [`hot-dev/hot-demos`](https://github.com/hot-dev/hot-demos) as standalone Hot projects with their own README. They're complete and runnable — they're recipes rather than full walkthroughs, so they live in the repo rather than in this docs site. - **Slack Bot** — multi-provider AI bot (Claude / GPT / Grok / Gemini) with live `!ai` switching and dual-mode polling-in-dev / webhooks-in-prod. [README](https://github.com/hot-dev/hot-demos/tree/main/slack-bot) · [Tutorial](https://hot.dev/blog/build-ai-slack-bot) - **My News** — scheduled job that fetches AI news sites in parallel, summarizes with Claude, and emails via Resend. A good first taste of schedules and `send(...)` triggers. [README](https://github.com/hot-dev/hot-demos/tree/main/my-news) - **Graph-RAG Memory** — the memory substrate behind the Hot Chat agents: raw records, capsules, graph nodes/edges, and hybrid retrieval with citations. Function-driven via `hot eval` rather than a full agent — useful for understanding how memory works under the hood. [README](https://github.com/hot-dev/hot-demos/tree/main/graph-rag-memory) ## How Demos Are Organized Each demo is a standalone Hot project: ```text demo-name/ hot.hot README.md .env.example hot/ src/ test/ ``` Demos use published Hot packages from the registry — clone, run `hot test` or `hot dev`, and dependencies resolve automatically. Some demos include `hot/ctx.hot` as a local-development convenience. `hot dev` loads that file to bridge values from `.env` into context variables, but Hot Dev Cloud ignores it. For deployed demos, add the same context variables in the Hot Dev App. ## What To Look For In The Hot App Most demos include an Agent Graph walkthrough. Open the Hot App, choose the demo agent, and inspect: - webhook, schedule, and MCP nodes that trigger handlers - event nodes created from `on-event` handlers - outgoing `sends` edges from literal `send(...)` calls or explicit `meta {sends: …}` declarations - loops that go from incoming request → event handler → memory write → reply If you only see a single trigger and one handler, that demo is intentionally small — the docs flag where to grow it next. --- Source: https://hot.dev/docs/demos/hot-chat # Hot Chat Demo Hot Chat is a complete, runnable demo of two AI agents and a polished web UI that drives them — all in one Hot project. It's the demo to point at when someone asks *"what does a product on Hot look like?"* - **Personal Mode** — identity-first memory. Notes follow the user across sessions, channels, and devices. - **Team Mode** — session-first memory. Two people in the same chat share one memory; two channels stay independent. - **One Next.js client** — a thin transport that publishes one typed event per message and renders the agent's reply over the run stream. Both agents live under `hot/src/` in the same project and boot together with one `hot dev`. The Next.js side is a thin transport — the agent is the product. **Expected time:** 15 minutes. **Cost:** none — the demo agents answer from local memory by default. Set `ANTHROPIC_API_KEY` for live LLM replies. ## What You'll Get To See - a clean chat UI that switches between Personal and Team modes live, - quick-prompt chips that map to slash commands without baking policy into the UI, - file attachments (drag-and-drop or paperclip) carried through to the agent as part of the same typed event, - a transparent identity panel so you can read off the exact `session_id` and `user_id` the agent will see, - per-command event handlers and streaming replies visible in the Agent Graph. ## Prerequisites - **Hot CLI** 2.0.3+ — [hot.dev/download](https://hot.dev/download) - **Node 20+** for the Next.js app - A **Hot API key** for your local dev environment (one-time, see below) No LLM API keys required — the demo agents answer from local memory. The project's `hot.hot` declares published packages (`hot.dev/hot-ai` **1.4.0**, `hot.dev/hot-ai-agent` **1.0.0**, `hot.dev/anthropic` **1.2.1**), so dependencies resolve from the Hot package registry automatically. ## Step 1: Clone ```bash git clone https://github.com/hot-dev/hot-demos cd hot-demos/hot-chat ``` ## Step 2: Verify The Project Compile and run the agent tests before booting the runtime: ```bash hot test ``` You should see the tests pass for both Personal and Team agents. This confirms the published deps resolve and both agents compile end to end. ## Step 3: Boot The Agents ```bash hot dev --open ``` `hot dev` opens the Hot App at and registers both agents under one project. Leave it running. While the Hot App is open, generate an API key: > *Hot App → API Keys → New Key.* Copy the value. ## Step 4: Start The Chat UI In a second terminal: ```bash cd hot-demos/hot-chat cp .env.example .env # paste the API key into HOT_API_KEY in .env npm install npm run dev ``` Open . The toolbar switches between Personal and Team modes live. ## Step 5: Walk Through The Modes ### Personal Mode (identity-first) Memory is keyed by **person** (`person:`), so it follows the user across sessions and devices. 1. Type `/remember I prefer launch updates that start with blockers` and press Enter. You'll see `remembered` stream into the assistant bubble. 2. Click **Recall preferences** (a quick-prompt chip) — the matching note comes back. 3. Refresh the browser and ask `/recall` again. Same answer — memory is keyed on you, not on the chat session. Personal Mode commands, grouped by role: | Role | Command | What it does | |-------------|--------------------|-----------------------------------------------------------| | Write | `/remember ` | store a personal note (free-chat does the same) | | Read | `/recall ` | search identity-scoped memory (deterministic, works offline) | | Synthesis | `/brief` | preferences, open tasks, deadlines, projects | | Synthesis | `/tasks` | open tasks only, rendered as a checklist | | Identity | `/whoami` | show transport, session, and user identity | | Help | `/guide` | cheat sheet of the available commands | ### Team Mode (session-first) Memory is keyed by **session** (`web:chat:`, `slack:T0:C0`, `telegram:-100…`), so two channels stay independent while two members of the same channel share one memory. 1. Switch to Team Mode in the toolbar. 2. Type *"we decided to ship docs before launch"*, then *"CI is the only blocker"*. 3. Ask `/ask what is blocking launch?` — the reply cites the matching records with attribution. Team Mode commands, grouped by role: | Role | Command | What it does | |------------|---------------|--------------------------------------------------------------------| | Write | (no command) | record the message into session memory | | Read | `/ask ` | LLM-backed answer grounded on channel memory | | Synthesis | `/summary` | distill the recent channel transcript | | Synthesis | `/decisions` | decisions, action items, and open questions from the transcript | | Identity | `/whoami` | show transport, session, and user identity | | Help | `/guide` | cheat sheet of the available commands | ## Step 6: Attach A File Drag a small file (text, image, PDF — under 4 MB) anywhere onto the chat. A chip appears below the composer. Send a message with it; the agent reply will include `… with 1 attachment(s)`. The agent stores the file's name and type as metadata; this demo doesn't deeply parse contents, but the same wire shape is how a real product would forward documents to your agent. ## Step 7: Inspect Identity Click **Identity** in the toolbar. You'll see the exact strings the agent receives: ```text Session person: ← Personal Mode web:chat: ← Team Mode User identity web:user: ``` That one difference — Personal Mode derives `session_id` from the identity, Team Mode trusts the caller's `session_id` — is the entire identity-first / session-first split made literal. Edit your display name and the agent picks it up on the next message. Identity is stored only in your browser's `localStorage` — clear site data to reset. ## Step 8: Open The Agent Graph In the Hot App, click into either agent and open the **Graph** tab. Each slash command shows up as its own typed event wired to its own handler: - `personal-agent:remember` → `remember` - `personal-agent:recall` → `recall` - `team-agent:ask` → `ask-question` - `team-agent:record` → `record-message` - …and so on, one node per command. There is no central dispatch function and no big `cond`. Add a command by writing one more `on-event` handler. ## Wire Contract The browser parses slash commands client-side and POSTs a typed event to the Next.js server route, which forwards it (with the API key) to Hot's `/v1/streams/subscribe-with-event`: ```json { "event_type": "team-agent:ask", "event_data": { "session_id": "web:chat:", "user_id": "web:user:", "user_name": "Demo User", "message_id": "web::", "timestamp": 1700000000, "question": "what's blocking launch?", "attachments": [{"name": "notes.md", "type": "text/markdown", "size": 412, "text": "…"}], "metadata": {"client": "hot-chat", "target": "team-agent"} } } ``` The matching `on-event` handler runs and emits `team-agent:reply:start` / `:delta` / `:end` stream events. The browser reads those and renders the assistant message as it arrives. A Slack or Telegram adapter can publish the same events from native message shapes — the wire contract is the contract. ## Project Layout ```text hot-chat/ src/ # Next.js app app/api/chat/route.ts # SSE proxy via @hot-dev/sdk/proxy lib/agent-client.ts # demo command map + @hot-dev/sdk/agent hot.hot # one project, two agents hot/ src/ personal-agent.hot # per-command event handlers team-agent.hot # per-command event handlers test/ personal-agent.hot team-agent.hot ``` Both agents are short, single-file projects. Diff them to see the *one* structural difference: Personal Mode derives `session_id` from the identity; Team Mode trusts the caller's `session_id`. ## Why This Architecture - **Browser → server route → Hot stream.** Auth, CORS, and rate-limiting can live in the Next.js route later without touching the browser code. - **No actions in the URL.** The UI passes free text and attachments; the agent decides what to do based on slash commands and typed events. - **Stable IDs.** `chatId` and `userId` come from `localStorage`, so memory follows the user across page reloads. Production would replace these with your auth system's identifiers. - **Per-command event handlers.** Each command is one `on-event` handler. The Agent Graph stays accurate as the agent grows. ## Build For Production ```bash npm run build npm start ``` The production build is what CI exercises. There's no agent-specific config in the build — point `HOT_API_URL` and `HOT_API_KEY` at any deployed Hot environment. ## Going Further The standalone demo keeps a smaller command surface so the source stays readable in one file each. The **full** TeamAgent and PersonalAgent in the main Hot repo (`hot/hot/src/team-agent/`, `hot/hot/src/personal-agent/`) add `/forget`, `/why`, `/export`, `/compact`, `/search`, `/stats`, `/diag`, `/ai`, scheduled digests, a `Researcher` peer, and more — production-shaped reference implementations for when you outgrow the demo. ## Source The runnable project lives in [hot-dev/hot-demos/hot-chat](https://github.com/hot-dev/hot-demos/tree/main/hot-chat). --- Source: https://hot.dev/docs/language # Hot Language Hot is an expression-oriented language for backend workflows. It keeps JSON-shaped data familiar, uses functions instead of infix operators, and provides flows for conditional and concurrent work. ## Quick Example ```hot ::myapp::hi ns greet fn (name: Str): Str { `Hello, ${name}!` } message greet("Hot") ``` ## What Looks Different | Other Languages | Hot | |----------------|-----| | `name = "Ada"` | `name "Ada"` | | `a + b` | `add(a, b)` | | `if (x) { } else { }` | `if(x, then, else)` or `cond { x => then => else }` | | `return value` | The final expression is the value | | `for x in items` | `map(items, ...)` or `for-each(iter, ...)` | ## Language Guide - **[Vars and Values](/docs/language/vars-and-values)** — Bindings, namespaces, immutable values, and deep paths - **[Data Literals](/docs/language/data-literals)** — Strings, numbers, vectors, maps, templates, and comments - **[Functions](/docs/language/functions)** — Functions, calls, lambdas, and lazy parameters - **[Types](/docs/language/types)** — Gradual typing, constructors, enums, unions, generics, and coercions - **[Error Handling](/docs/language/errors)** — Results, propagation, and explicit failure handling - **[Flows](/docs/language/flows)** — Serial, conditional, matching, pipe, and parallel flows - **[Language Evaluation Model](/docs/language/execution-model)** — How those rules compose while Hot code is running - **[What Hot Doesn't Have](/docs/language/not-supported)** — Conventional syntax and constructs that Hot replaces --- Source: https://hot.dev/docs/language/execution-model # Language Evaluation Model This page describes how Hot evaluates code **within one platform run or task attempt**. Events, handler routing, retries, tasks, workers, and stream lineage belong to the [Platform Execution Model](/docs/platform/execution-model). Within Hot code, the key is to keep separate concepts separate: eager versus lazy describes **when an argument is evaluated**, while serial versus parallel describes **how a flow schedules its contents**. ## Model at a Glance | Concern | Default | Explicit alternative | Detailed reference | |---------|---------|----------------------|--------------------| | Function arguments | Evaluated before the call | A `lazy` parameter defers one argument | [Functions](/docs/language/functions#lazy-arguments) | | Function bodies | Implicit `serial` flow | `parallel`, `cond`, `match`, and other flows | [Flows](/docs/language/flows) | | Bindings and values | Immutable lexical bindings | Name reuse and deep paths create later bindings | [Vars and Values](/docs/language/vars-and-values) | | Failure | A bound `Result` remains intact; ordinary consumption propagates an `Err` | Lazy Result helpers or `match` inspect failure explicitly | [Error Handling](/docs/language/errors) | | Types | Gradual checking with structural records and tagged constructed values | Annotations narrow known contracts; `Any` remains dynamic | [Types](/docs/language/types) | ## How the Rules Compose For an ordinary call, Hot evaluates non-lazy argument expressions, enters an implicit serial body, and uses the body's final expression as the function's success value. There is no `return` statement. Flows such as `cond`, `match`, and `parallel` are expressions too, so they can provide that final value or be bound within a larger serial body. A `lazy` parameter changes argument evaluation only. Each `do` forces its deferred computation again; it does not turn the surrounding body into a parallel flow. Likewise, `Iter` defers sequence-element production rather than function-argument evaluation. See [Functions](/docs/language/functions#lazy-arguments) for lazy parameters and the [`Iter` package](/pkg/hot-std/hot/iter) for sequence laziness. An explicit `parallel` flow changes scheduling within that flow. Hot derives dependencies from binding references and can run independent bindings concurrently. It does not parallelize ordinary serial code. Result collection, dependency levels, failure behavior, and `All` / `All` annotations are defined in [Flows](/docs/language/flows#parallel-flow). ## Results at Evaluation Boundaries Binding a `Result` does not consume it. Ordinary function arguments, template interpolation, and ordinary field or index access are consumption boundaries: an `Ok` supplies its payload and an `Err` propagates. Result-aware helpers make their boundary explicit: `is-*` inspects the intact variant, while `if-*` selects a handler and supplies its payload. A `match` selects an arm from the variant identity, and the matched name exposes that variant's payload inside the arm. Function return annotations name the successful value rather than a visible `Result` wrapper. See [Error Handling](/docs/language/errors) for creation, propagation, inspection, `err(...)`, and `fail(...)`. ## Values, Types, and Effects Hot uses immutable value semantics and lexically scoped closures. Reusing a name or assigning through a deep path produces a later binding rather than mutating a shared object. [Vars and Values](/docs/language/vars-and-values) defines those operations. Hot is gradually typed: it checks statically visible information while allowing dynamic boundaries through `Any`. [Types](/docs/language/types) owns the detailed rules for structural records, nominal runtime tags, unions, generics, and coercions. Hot's value and control model is functional, but the language is not pure. HTTP, files, stores, tasks, events, and other external effects can occur in ordinary functions, and the type system does not track them. Parallel dependency analysis sees Hot binding references, not conflicts in external state; already-started sibling effects cannot be rolled back automatically. See [Flows](/docs/language/flows#parallel-flow) for the resulting coordination and idempotency guidance. --- Source: https://hot.dev/docs/language/vars-and-values # Vars and Values In Hot, everything is either a **Var** (a named binding) or a **Value**. This simple model is the foundation of the language. ## Values A Value is anything that can be bound to a Var. Values fall into three categories: **data**, **definitions**, and **references**. ### Data The most common Values are literal data: | Type | Example | Description | |------|---------|-------------| | `Str` | `"hello"`, `` `template` ``, `"""block"""`, `` ```block template``` `` | Text strings (template strings support `${}` interpolation, block strings are indent-aware) | | `Int` | `42` | Whole numbers | | `Dec` | `19.99` | Decimal numbers (not floats!) | | `Bool` | `true`, `false` | Boolean values | | `Null` | `null` | Absence of value | | `Vec` | `[1, 2, 3]` | Ordered collections (vectors) | | `Map` | `{a: 1}` | Key-value collections (like objects) | | `Fn` | `(x) { x }` | Anonymous functions | ### Definitions Values can also be **definitions** — they introduce new types or named functions: ```hot // Function definition greet fn (name: Str): Str { `Hello, ${name}!` } // Type definition User type { id: Str, email: Str, active: Bool } // Enum definition Direction enum { Up, Down, Left, Right } ``` Definitions are still Values—a `fn` definition produces a function Value, and a `type` definition produces a type constructor. ### References A Value can be a **reference** to a Var or namespace defined elsewhere: ```hot // Namespace alias — binds a short name to an existing namespace ::http ::hot::http // Var import — binds a local name to a Var from another namespace send-email ::notifications::email/send // Var reference — binds a new name to an existing Var's Value role ::myapp::users/default-role ``` References are covered in more detail in the [Namespaces](#namespaces) section below. All Values are **immutable**. You don't modify data; you create new data. ## Vars A Var is a name bound to a Value. Unlike most languages, Hot uses **no `=` sign** for assignment: ```hot // Var assignment: name followed by value name "Alice" age 30 items [1, 2, 3] ``` Think of it as "name is Value" rather than "name equals Value". ### With Type Annotations You can optionally add a type between the name and Value: ```hot name-typed: Str "Alice" count: Int 42 prices: Vec [9.99, 19.99, 29.99] ``` ### Deep Path Assignment Assign to nested paths to build up Maps: ```hot user.name "Bob" user.email "bob@example.com" user.settings.theme "dark" user.settings.notifications true ``` Despite the assignment-like syntax, this does not mutate a shared object. Hot constructs or immutably updates the collection and creates a later binding for its root name. A lambda captures the current values of outer names when it is created, so a later deep-path assignment does not change an already captured value. A named `fn` instead resolves an outer name from the current binding each time it runs. Deep paths also work with Vec indices: ```hot config.servers[0] "api.example.com" config.servers[1] "db.example.com" config.backends[0].host "api.internal" config.backends[1].host "db.internal" config.retries 3 ``` When a missing path is followed by a literal integer index, Hot creates a Vec. Assigning past its current end grows the Vec and fills intervening positions with `null`, subject to the runtime collection-size limit: ```hot ports [4680] ports[3] 4683 dynamic-ports [4680] port-index 3 dynamic-ports[port-index] 4683 ``` Bracket paths can be dynamic. A bracket segment accepts an integer or string literal, or a bare variable whose value is used as a Map key or, for an integer applied to a Vec, as its index. It is not a general expression position; compute a more complex key first and bind it to a name. Because a dynamic key does not tell the compiler which collection to create, its parent must already exist. Paths can continue after the dynamic segment: ```hot profile {name: "Ada"} field "name" profile[field] "Grace" workers [{status: "idle"}, {status: "idle"}] index 1 workers[index].status "ready" ``` ### Appending to Vectors Use empty brackets to append to a vector: ```hot // Use empty brackets to append to a vector list[] "first" list[] "second" list[] "third" ``` ```hot // list = ["first", "second", "third"] ``` Like all Hot operations, this follows immutable semantics. Each append creates a **new** vector and rebinds the variable—it doesn't mutate the original. The syntax is shorthand for: ```hot list [] // list = [] list concat(list, ["first"]) // list = ["first"] (new vector, rebind) list concat(list, ["second"]) // list = ["first", "second"] (new vector, rebind) ``` This also works with nested paths: ```hot // Append to nested vectors shopping.items[] "apple" shopping.items[] "banana" shopping.items[] "cherry" ``` ```hot // shopping = {items: ["apple", "banana", "cherry"]} ``` Each nested append creates a new outer structure with the updated inner vector. ### Deep Path Access Read from nested paths into a new Var: ```hot user-nested {name: "Alice", settings: {theme: "dark"}} // Deep-get access: assign nested value to a new var theme user-nested.settings.theme ``` ```result user-nested.settings.theme → "dark" ``` This works with Vec indices too: ```hot servers ["api.example.com", "db.example.com"] primary first(servers) ``` ## Namespaces Every Var lives in a namespace. Each Hot file declares its namespace with `ns` keyword: ```hot ::myapp::users ns // These Vars are in the ::myapp::users namespace default-role "member" max-users 1000 ``` ### Referencing Vars Use the full path `::namespace/var-name` to reference Vars from other namespaces: ```hot // Reference a Var from another namespace role ::myapp::users/default-role ``` ### Namespace Aliases Create shorter names for frequently-used namespaces: ```hot // Create aliases ::http ::hot::http ::env ::hot::env // Now use the short form api-url ::env/get("API_URL", "https://api.example.com") response ::http/get(api-url) ``` ### Importing Specific Items Import individual Vars/functions into your namespace: ```hot ::myapp::handlers ns // Import specific items HttpResponse ::hot::http/HttpResponse send-email ::notifications::email/send // Use without namespace prefix response HttpResponse({status: 200}) ``` ## Core Vars Vars marked with `core: true` in their metadata are available everywhere without namespace qualification. Hot's standard library uses this extensively: ```hot ::myapp::example ns // These are core - no prefix needed doubled map([1, 2, 3], mul(%, 2)) total add(1, 2) name Str(42) // Equivalent to: // doubled ::hot::coll/map(...) // total ::hot::math/add(...) // name ::hot::type/Str(...) ``` Core functions from hot-std include: `map`, `filter`, `reduce`, `add`, `sub`, `mul`, `div`, `eq`, `lt`, `gt`, `if`, `and`, `or`, `not`, `Str`, `Int`, `Dec`, `ok`, `err`, and many more. ### Making Your Own Core Vars You can mark your own Vars as core to make them available throughout your application without namespace prefixes. This is powerful for domain-specific languages or frequently-used utilities: ```hot ::myapp::domain ns // Mark a function as core send-notification meta { core: true, doc: "Send a notification to a user." } fn (user-id: Str, message: Str): Result { // implementation } // Mark a type constructor as core UserId meta {core: true} type fn (id: Str): UserId { id } ``` Now any file in your application can use these without imports: ```hot ::myapp::handlers ns // No import needed - these are core result send-notification("user-123", "Welcome!") id UserId("user-456") ``` This lets you bend the language to your domain, making common operations feel built-in. ## Immutability Bindings and values are immutable. Reusing a name creates a later binding: ```hot count 1 count 2 // This creates a NEW binding, shadowing the first ``` This isn't "changing" count—it's creating a new Var that shadows the previous one. In most contexts, you'll work with transformations that produce new Values: ```hot numbers [1, 2, 3] doubled-nums map(numbers, (x) { mul(x, 2) }) // [2, 4, 6] // 'numbers' is still [1, 2, 3] ``` ```result numbers → [1, 2, 3] // Original unchanged doubled-nums → [2, 4, 6] // New value returned ``` ## Summary - **Values** are data (strings, numbers, vectors, maps...), definitions (`fn`, `type`, `enum`), or references to other namespaces and Vars - **Vars** bind names to Values using `name Value` syntax (no `=`) - **Namespaces** organize Vars with `::path::to::namespace` - **Core** functions are available everywhere without qualification - Everything is **immutable** — create new Values, don't modify existing ones --- Source: https://hot.dev/docs/language/data-literals # Data Literals Hot uses JavaScript-like syntax for data literals, making it instantly familiar if you've worked with JSON. There are a few important differences to be aware of. ## Strings Double-quoted strings, just like JSON: ```hot greeting "Hello, world!" path "/api/users" empty-str "" ``` ### Template Strings Use backticks for string interpolation: ```hot name-tpl "Alice" message `Hello, ${name-tpl}!` // "Hello, Alice!" calculation `2 + 2 = ${add(2, 2)}` // "2 + 2 = 4" ``` ```result `Hello, ${name}!` → "Hello, Alice!" `2 + 2 = ${add(2, 2)}` → "2 + 2 = 4" ``` Any expression can go inside `${}`. ### Block Strings For content where you don't want escape processing, use block strings (`"""`): ```hot simple-tq """Hello, world!""" multiline-tq """ SELECT * FROM users WHERE active = true """ ``` Block strings have **no escape processing** — escape sequences (`\n`, `\"`, etc.) are not interpreted, so characters are treated as literal text. They're also **indent-aware**: the closing `"""` determines the base indentation, which is automatically stripped from all lines. This makes them ideal for documentation, SQL queries, HTML templates, and embedded code examples. ### Block Template Strings For multi-line content that needs both indent-awareness and `${}` interpolation, use block template strings: ```hot table-name "users" max-rows 100 query-tbt ``` SELECT * FROM ${table-name} WHERE active = true LIMIT ${max-rows} ``` ``` Block template strings combine the best of both worlds: **indent-aware** like block strings (the closing ` ``` ` determines the base indentation to strip) and **interpolation** like template strings (`${}` expressions are evaluated). Like block strings, they have **no escape processing**. This makes them ideal for shell scripts, HTML templates, and embedded code that needs Hot variables. ## Comments Use `//` for a line comment and `/* ... */` for a block comment. Block comments can span multiple lines or appear between expressions: ```hot // Explain why this value is needed. timeout 30 /* * Block comments are useful for longer context. */ name /* inline block comment */ "Ada" ``` ## Numbers Hot has two number types: **Int** and **Dec**. ### Int (Integers) Whole numbers without decimal points: ```hot count-num 42 negative -17 zero-num 0 ``` ### Dec (Decimals) Numbers with decimal points. Hot uses `Dec` instead of floating-point: ```hot price 19.99 rate 0.05 pi 3.14159265358979323846 ``` **Why Dec instead of Float?** Floating-point math has precision issues: ```javascript // In JavaScript: 0.1 + 0.2 = 0.30000000000000004 ``` Hot's `Dec` type uses 256-bit decimal arithmetic (via [fastnum D256](https://docs.rs/fastnum)) providing up to **76 digits of precision**. This means exact decimal arithmetic—critical for money, percentages, scientific calculations, and anywhere precision matters. ```hot // These are exact in Hot, no floating-point errors total-dec add(0.1, 0.2) // Exactly 0.3 ``` ```result add(0.1, 0.2) → 0.3 // Exact, no floating-point error! ``` ## Booleans ```hot active true disabled false ``` ## Null The absence of a value: ```hot nothing null user-avatar null ``` ## Vectors Ordered collections using square brackets: ```hot numbers [1, 2, 3] names ["Alice", "Bob", "Carol"] mixed ["text", 42, true, null] empty-vec [] ``` Nested vectors: ```hot matrix [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] ``` Access elements with `first()`, `last()`, or index notation: ```hot numbers-access [10, 20, 30] first-num first(numbers-access) // 10 last-num last(numbers-access) // 30 ``` > **Note:** Hot uses `Vec` (vector) where other languages use "array". Same concept, different name. ### Vec Spread Use `...` to flatten existing vectors into a new vector literal: ```hot a [1, 2, 3] b [4, 5] combined [...a, ...b] // [1, 2, 3, 4, 5] with-extras [0, ...a, 99] // [0, 1, 2, 3, 99] clone [...a] // [1, 2, 3] (shallow copy) ``` Spread elements are flattened inline. Non-spread elements and spread elements can be freely mixed. For simple concatenation without extra elements, you can also use `concat(a, b)`. ## Maps (like Objects) Key-value collections using curly braces: ```hot user-map {name: "Alice", age: 30} config-map {debug: true, port: 8080} empty-map {} ``` Nested maps: ```hot settings { database: { host: "localhost", port: 5432 }, cache: { enabled: true, ttl: 3600 } } ``` Access properties with dot notation: ```hot user-access {name: "Alice", email: "alice@example.com"} name-access user-access.name // "Alice" email-access user-access.email // "alice@example.com" ``` ```result user.name → "Alice" user.email → "alice@example.com" ``` **Map vs Object**: Hot calls these `Map` instead of Object. Map-literal keys use identifier or string syntax. Dynamic bracket access can also use a runtime key; dot access is always a string-field lookup. ### Map Spread Use `...` to merge existing maps into a new map literal: ```hot defaults {timeout: 5000, retries: 3} config {...defaults, retries: 5} // {timeout: 5000, retries: 5} ``` Later keys win, so spread entries can be selectively overridden. Multiple spreads and explicit keys can be mixed in any order. ## Comparison to JavaScript/JSON | Concept | JavaScript/JSON | Hot | |---------|----------------|-----| | Array | `[1, 2, 3]` | `[1, 2, 3]` (same syntax, called `Vec`) | | Object | `{"a": 1}` | `{a: 1}` (unquoted keys, called `Map`) | | String | `"hello"` | `"hello"` (same!) | | Template string | `` `Hi ${name}` `` | `` `Hi ${name}` `` (same!) | | Block string | N/A | `"""..."""` (no escaping, indent-aware) | | Block template string | N/A | `` ```...``` `` (indent-aware `${}` interpolation) | | Integer | `42` | `42` (same!) | | Float | `3.14` | `3.14` (but it's `Dec`!) | | Boolean | `true`/`false` | `true`/`false` (same!) | | Null | `null` | `null` (same!) | ## Type Annotations on Literals You can add types to your data for documentation and type checking: ```hot // Simple types count-typed: Int 42 price-typed: Dec 19.99 name-str: Str "Alice" // Generic types numbers-typed: Vec [1, 2, 3] prices-typed: Vec [9.99, 19.99] user-typed: Map {name: "Alice", age: 30} ``` ## Summary - Hot's data literals are **nearly identical to JSON** - `Vec` (vector) for ordered collections, `Map` for key-value collections - Use `Dec` for decimals (not floating-point) — exact precision - Template strings use backticks with `${expression}` interpolation - Block strings (`"""..."""`) — no escape processing, indent-aware - Block template strings (`` ```...``` ``) for indent-aware content with `${}` interpolation - Comments use `//` for one line or `/* ... */` for a block - Access vectors with `[index]`, maps with `.property` --- Source: https://hot.dev/docs/language/functions # Functions In Hot, everything is a [Var/Value pair](/docs/language/vars-and-values)—function names are Vars, and function definitions are Values. ## Defining Functions Use `fn` followed by parameters and a body: ```hot greet fn (name: Str): Str { `Hello, ${name}!` } add-numbers fn (a: Int, b: Int): Int { add(a, b) } ``` ```result greet("World") → "Hello, World!" add-numbers(5, 3) → 8 ``` The last expression in the body is the return value—no `return` keyword needed. ### Functions are Flows The `fn` keyword modifies a **flow** to turn it into a function definition. The default flow is `serial`, which can be omitted: ```hot // These are equivalent: greet-v1 fn (name: Str): Str { `Hello, ${name}!` } greet-v2 fn serial (name: Str): Str { `Hello, ${name}!` } ``` You can use other flow types to change how the function body executes: ```hot // Conditional function using cond flow is-admin fn cond (user): Bool { eq(user.role, "admin") => { true } => { false } } ``` See [Flows](/docs/language/flows) for more on `serial`, `parallel`, `cond`, and `cond-all`. ## Calling Functions Call functions with parentheses: ```hot result-greet greet("World") // "Hello, World!" sum add-numbers(5, 3) // 8 ``` ## Qualified Calls Call functions from other namespaces using the full path: ```hot upper ::hot::str/uppercase("hello") // "HELLO" data ::hot::http/get("https://api.example.com") ``` Or create a function alias: ```hot uppercase ::hot::str/uppercase result uppercase("hello") // "HELLO" ``` Or create a namespace alias: ```hot ::str ::hot::str result ::str/uppercase("hello") // "HELLO" ``` ## Parameter Types Types are optional but recommended: ```hot // Fully typed greet-typed fn (name: Str): Str { `Hello, ${name}!` } // Untyped (accepts anything) echo fn (x) { x } // Mixed process fn (data: Map, options): Map { data } ``` ## Nullable Parameters Use `?` for types that accept null (shorthand for `Type | Null`): ```hot greet-titled fn (name: Str, title: Str?): Str { if(title, `Hello, ${title} ${name}!`, `Hello, ${name}!`) } ``` ```result greet-titled("Alice", null) → "Hello, Alice!" greet-titled("Smith", "Dr") → "Hello, Dr Smith!" ``` ## Function Overloading Define multiple versions of a function with different parameter counts (arity): ```hot slice fn (coll: Vec, start: Int): Vec { // slice from start to end (stub) coll }, (coll: Vec, start: Int, end: Int): Vec { // slice from start to end (stub) coll } ``` Or different parameter types: ```hot process-data fn (x: Int): Str { `Integer: ${x}` }, (x: Str): Str { `String: ${x}` }, (x: Vec): Str { `Vector with ${length(x)} items` } ``` Hot dispatches to the correct version based on arguments. ## Variadic Functions Accept any number of arguments with `...`: ```hot concat-all fn (first-vec: Vec, ...rest: Vec): Vec { reduce(rest, (acc, v) { concat(acc, v) }, first-vec) } ``` ## Lambdas (Anonymous Functions) Create inline functions with `(params) { body }`: ```hot doubled map([1, 2, 3], (x) { mul(x, 2) }) sum-lambda reduce([1, 2, 3], (acc, x) { add(acc, x) }, 0) ``` ```result map([1, 2, 3], (x) { mul(x, 2) }) → [2, 4, 6] reduce([1, 2, 3], (acc, x) { add(acc, x) }, 0) → 6 ``` Lambdas are just values—assign them to vars: ```hot double-fn (x) { mul(x, 2) } result-double double-fn(5) // 10 ``` ### Placeholder Lambdas (`%`) For single-parameter lambdas, use `%` as shorthand. Hot automatically wraps the expression in a lambda: ```hot // These are equivalent: map([1, 2, 3], (x) { mul(x, 2) }) map([1, 2, 3], mul(%, 2)) // Property access: filter(users, (u) { u.active }) filter(users, %.active) // In pipelines: names |> map(%.email) |> filter(ends-with(%, "@company.com")) ``` For multi-parameter lambdas, use `%1`, `%2`, etc. (bare `%` is the same as `%1`): ```hot reduce([1, 2, 3], add(%1, %2), 0) // 6 ``` #### Explicit Lambda Boundary: `%(expr)` When `%` appears inside nested function calls, the implicit lambda wraps at the outermost call boundary. If that's not what you want, use `%(expr)` to mark exactly where the lambda should be created: ```hot // Without %(expr) — implicit wrapping binds at sort, not map (wrong) // sort(map(items, %.value)) // With %(expr) — lambda wraps at map (correct) sort(map(items, %(%.value))) length(filter(items, %(gt(%, 3)))) ``` Rule of thumb: if `%` is an argument to a function that is **itself** an argument to another function, use `%(...)`. Use explicit `(params) { body }` when: - The lambda has multiple statements - The parameter is unused (side-effect only) - Clarity is more important than brevity ## Lazy Arguments Ordinary function arguments are evaluated before the function body runs. Arguments marked `lazy` instead arrive as deferred computations. This enables short-circuit evaluation: ```hot if fn cond (pred: Any, lazy then: Any): Any { pred => { do then } }, cond (pred: Any, lazy then: Any, lazy else: Any): Any { pred => { do then } => { do else } } ``` Use `do` to evaluate a lazy argument: ```hot safe-access fn (data, lazy fallback) { if(data, data, do fallback) } ``` This is how `if`, `and`, and `or` avoid evaluating unused branches. Lazy arguments are not memoized. Each `do` forces the deferred computation again. Bind the result of `do` if it will be used more than once, especially when the argument performs I/O or another side effect: ```hot use-twice fn (lazy value: Any): Vec { forced do value [forced, forced] } ``` This argument-level laziness is separate from `Iter`: a lazy parameter defers one expression, while an iterator produces sequence elements on demand. ## Metadata on Functions Add documentation, test markers, or event handlers: ```hot // Documentation greet meta {doc: "Greets a user by name"} fn (name: Str): Str { `Hello, ${name}!` } // Test function test-greet meta ["test"] fn () { assert-eq(greet("World"), "Hello, World!") } // Event handler on-user-created meta {on-event: "user:created"} fn (event) { send-welcome-email(event.data.email) } // Scheduled function daily-cleanup meta {schedule: "@daily"} fn (event) { cleanup-old-records() } ``` ## Core Functions These functions are available everywhere without imports. See [hot-std](/pkg/hot-std) for full documentation. **[Math](/pkg/hot-std/hot/math)**: `add`, `sub`, `mul`, `div`, `mod`, `pow`, `round`, `floor`, `ceil`, `rand` **[Comparison](/pkg/hot-std/hot/cmp)**: `eq`, `ne`, `lt`, `gt`, `lte`, `gte` **[Logic](/pkg/hot-std/hot/bool)**: `if`, `and`, `or`, `not`, `is-truthy` **[Collections](/pkg/hot-std/hot/coll)** (eager): `map`, `filter`, `reduce`, `first`, `rest`, `last`, `length`, `concat`, `flatten`, `merge`, `keys`, `vals`, `some`, `all`, `range`, `sort`, `reverse`, `distinct`, `slice` **[Iterators](/pkg/hot-std/hot/iter)** (lazy): `Iter`, `next`, `collect`, `for-each`, `take`, `range` **[Strings](/pkg/hot-std/hot/str)**: `uppercase`, `lowercase`, `trim`, `split`, `join`, `starts-with`, `ends-with`, `contains`, `replace` **[Results](/pkg/hot-std/hot/type)**: `ok`, `err`, `is-ok`, `is-err`, `Result` **[Types](/pkg/hot-std/hot/type)**: `Str`, `Int`, `Dec`, `Bool`, `Vec`, `Map`, `Any`, `Null`, `is-null`, `is-some` ## Tail Call Optimization (TCO) Hot automatically optimizes tail-recursive functions, enabling stack-safe recursion for any depth. A call is in **tail position** when its result is returned directly without further processing: ```hot factorial fn cond (n: Int, acc: Int): Int { lte(n, 1) => { acc } => { factorial(sub(n, 1), mul(n, acc)) } } ``` ```result factorial(5, 1) → 120 factorial(1, 1) → 1 ``` Use the accumulator pattern to make functions tail-recursive: ```hot // NOT tail-recursive (result passed to add, not returned) sum fn (xs: Vec): Int { if(is-empty(xs), 0, add(first(xs), sum(rest(xs)))) } // Tail-recursive with accumulator (stack-safe) sum fn (xs: Vec): Int { sum-acc(xs, 0) } sum-acc fn cond (xs: Vec, acc: Int): Int { is-empty(xs) => { acc } => { sum-acc(rest(xs), add(acc, first(xs))) } } ``` ## Summary - `fn` modifies a flow to become a function (default is `serial`, omittable) - Use `fn cond` for conditional functions, `fn parallel` for concurrent execution - Call with no space before `(`: `func(args)` - Overload by arity or type - Use lambdas `(x) { body }` for inline functions, or `%` for concise single-param lambdas - Mark args `lazy` for deferred evaluation - Tail-recursive functions are automatically optimized (TCO) --- Source: https://hot.dev/docs/language/meta # 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: ```hot greet-meta meta {doc: "Greets a user by name"} fn (name: Str): Str { `Hello, ${name}!` } ``` Multiple fields: ```hot process-meta meta { doc: "Process incoming data", core: true } fn (data: Any): Any { data } ``` ### Vector Form Use `meta [...]` for simple tags: ```hot test-greet-demo meta ["test"] fn () { assert-eq(greet-meta("World"), "Hello, World!") } ``` ## Documentation The `doc` field provides documentation for functions and types: ```hot 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: ```hot // 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: ```hot ::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: ```hot ::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}` } ``` ```hot ::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"]`: ```hot 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: ```hot 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: ```hot 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: - Cron expressions: `"0 2 * * *"`, `"*/5 * * * *"` (5, 6, or 7 fields) - Nicknames: `"@daily"`, `"@hourly"`, `"@weekly"` - Intervals: `"every N seconds"`, `"every N minutes"`, `"every N hours"` - Natural language: `"every day at 9:00 am"`, `"on Sunday at 12:00"` All times are UTC. See [Schedules](/docs/schedules) for the full format reference and dynamic (runtime-created) schedules. ## MCP Tools The `mcp` field exposes a function as a [Model Context Protocol](/docs/mcp) tool, making it callable by AI models and agents: ```hot 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: | Field | Required | Description | |-------|----------|-------------| | `service` | Yes | Groups tools into a named service with its own endpoint | | `auth` | No | `"required"` (default) or `"none"`. Controls whether Hot validates credentials before invocation. | | `name` | No | Override the auto-generated tool name | | `description` | No | Human-readable description (helps AI choose the right tool) | | `title` | No | Display title | | `input-schema` | No | Override auto-generated input JSON Schema | | `output-schema` | No | Override auto-generated output JSON Schema | | `annotations` | No | MCP 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](/docs/mcp) for the full reference on services, schemas, endpoints, and best practices. ## Webhook Endpoints The `webhook` field exposes a function as a [webhook endpoint](/docs/webhooks), allowing external services to send HTTP requests to your Hot functions: ```hot 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: | Field | Required | Description | |-------|----------|-------------| | `service` | Yes | Groups endpoints into a named service (part of the URL) | | `path` | Yes | URL path within the service (e.g., `/events`) | | `method` | No | HTTP method to match (default: `POST`) | | `name` | No | Override the auto-generated endpoint name | | `description` | No | Human-readable description | | `auth` | No | `"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](/docs/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: ```hot 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): ```hot 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: ```hot 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](/docs/retries) for retry fields, backoff behavior, and platform limits. ## Context Requirements The `ctx` field declares context variables (secrets and configuration) that a namespace requires: ```hot ::myapp::api ns meta {ctx: { "openai.api.key": {required: true}, "rate.limit": {required: false, default: 1000, secret: false} }} ``` ### Per-Key Properties | Property | Type | Default | Description | |----------|------|---------|-------------| | `required` | bool | `true` | Must be provided at runtime | | `default` | any | none | Value if not provided (implies `required: false`) | | `secret` | bool | `true` | If true, value will be masked in call logs | ### Examples **Required secret (most common):** ```hot meta {ctx: {"anthropic.api.key": {required: true}}} ``` **Optional with default (non-secret):** ```hot meta {ctx: {"rate.limit": {default: 60, secret: false}}} ``` **Multiple keys:** ```hot 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 `""` 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: ```hot // 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: ```hot ::myapp::test::users meta ["test"] ns // All functions in this namespace are test-related ``` ## Combining Metadata Combine multiple metadata fields in one map: ```hot process-order meta { doc: "Process an incoming order", on-event: "order:created", core: true } fn (event) { // ... } ``` ## Summary | Metadata | Purpose | |----------|---------| | `doc: "..."` | Documentation | | `core: true` | Globally 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) | --- Source: https://hot.dev/docs/language/types # Types Hot is gradually typed. Add annotations where they help catch errors and document intent; skip them where they add noise. Hot infers types when it has enough information and uses `Any` when a value's type or shape cannot be established statically. The model is deliberately hybrid: - Plain records are checked structurally when their fields are known. - Constructed types and enum variants retain nominal runtime tags. - Literal and union types narrow the accepted values. - `Any` is the dynamic escape hatch at partially typed boundaries. ## Built-in Types | Type | Description | Example | |------|-------------|---------| | `Str` | Text strings | `"hello"`, `` `template` ``, `"""block"""`, `` ```block template``` `` | | `Int` | Integers | `42` | | `Dec` | Decimal numbers | `19.99` | | `Bool` | Booleans | `true`, `false` | | `Null` | Null value | `null` | | `Vec` | Vectors (also known as arrays) | `[1, 2, 3]` | | `Map` | Maps (objects) | `{a: 1}` | | `Fn` | Functions | `(x) { x }` | | `Any` | Any type | anything | | `Bytes` | Binary data | — | ## Type Annotations Add types to variables: ```hot name: Str "Alice" count: Int 42 prices: Vec [9.99, 19.99] ``` Add types to function parameters and returns: ```hot greet-typed fn (name: Str): Str { `Hello, ${name}!` } process fn (items: Vec, multiplier: Int): Vec { map(items, (x) { mul(x, multiplier) }) } ``` ```result greet-typed("World") → "Hello, World!" process([1, 2, 3], 2) → [2, 4, 6] ``` ## Types are Optional You can skip types entirely: ```hot // No types - still valid Hot name-simple "Alice" greet-simple fn (name) { `Hello, ${name}!` } ``` Hot will infer types where possible and allow `Any` elsewhere. This is best-effort static checking rather than whole-program proof. A known record can be checked against a required field shape, while an untyped `Map` may be accepted and fail later when code accesses a missing field. Add annotations at API, storage, event, and other trust boundaries when you want stronger diagnostics. ## Generic Types Parameterize collection types: ```hot names-generic: Vec ["Alice", "Bob"] counts-generic: Map {apples: 5, oranges: 3} matrix-generic: Vec> [[1, 2], [3, 4]] ``` ## Union Types A value can be one of several types: ```hot parse-number fn (input: Str): Int | Dec | Null { // Returns Int, Dec, or null } process fn (value: Str | Int): Str { Str(value) } ``` ### Literal Unions Union types can include literal values: ```hot // String literals Fruit type "apple" | "banana" | "orange" // Number literals DiceRoll type 1 | 2 | 3 | 4 | 5 | 6 // Mixed literals Status type "pending" | "active" | 0 | 1 // Use in functions pick-fruit fn (fruit: Fruit): Str { `You picked a ${fruit}!` } apple: Fruit "apple" // Valid pick-fruit("banana") // Valid pick-fruit("grape") // Type error ``` Literal unions let you restrict values to a specific set of allowed values at the type level. ## Nullable Types Use `?` for values that might be null: ```hot find-user fn (id: Str): User? { // Returns User or null } greet fn (name: Str, title: Str?): Str { // title can be a Str or null } ``` `Str?` is shorthand for `Str | Null`. > **Note:** The `?` syntax indicates the *type* accepts null—it does not make the parameter omittable. You must still pass an argument (either a value or `null`). For truly optional parameters, use function overloading. > **Why no `Option` type?** Languages without null (like Rust) use `Option` with `Some(value)` and `None` to represent optional values. Hot has null to align with JavaScript/JSON data types, so `T?` achieves the same thing more concisely. While `Option` can technically express one extra state (`Some(null)` vs `None`), this distinction is rarely needed in practice and adds complexity for everyone. If an `Option` type is needed, you can define a custom type and make it [core](/docs/language/meta#core-functions) to your codebase. ## Defining Custom Types ### Struct Types Define a type with fields: ```hot Person type { name: Str, age: Int, email: Str? } ``` ### Types Are Constructors The type name is also its constructor function: ```hot Point type { x: Int, y: Int } // Create an instance - type name IS the constructor alice-point Point({x: 10, y: 20}) ``` ```result alice-point.x → 10 alice-point.y → 20 ``` > **Key Concept:** In Hot, **types are functions that return values of that type**. When you define a type, you're also defining a constructor function with the same name. This unifies type definitions and value creation into a single concept. ### Types with Custom Constructors Add a constructor function for validation or convenience by combining the struct definition with a function: ```hot // Struct definition + constructor function in single declaration Point2D type { x: Int, y: Int } fn (x: Int, y: Int): Point2D { Point2D({x: x, y: y}) } ``` ```result Point2D(10, 20) → {x: 10, y: 20} ``` You can also define multiple constructor arities: ```hot // Multiple constructor arities Range type { start: Int, end: Int } fn (end: Int): Range { Range({start: 0, end: end}) }, (start: Int, end: Int): Range { Range({start: start, end: end}) } ``` ```result Range(10) → {start: 0, end: 10} Range(5, 15) → {start: 5, end: 15} ``` ### Empty Types (Markers) Types with no fields work as markers or tags: ```hot Token type { } Admin type { } token Token() admin Admin() ``` ## Enums (Variant Unions) Define a type with multiple named variants using the `enum` keyword: ```hot // Simple variants (no data) Direction enum { Up, Down, Left, Right } // Create values up-dir Direction.Up down-dir Direction.Down ``` ### Variants with Data Variants can carry data by referencing other types: ```hot // Define the payload types first Circle type { radius: Dec } Rectangle type { width: Dec, height: Dec } // Define the enum Shape enum { Point, Circle(Circle), Rectangle(Rectangle) } // Create values shape-point Shape.Point shape-circle Shape.Circle({radius: 5.0}) shape-rect Shape.Rectangle({width: 10.0, height: 20.0}) ``` ### Built-in Variant Types Hot uses variant unions for core types: ```hot // Result has Ok and Err variants success Result.Ok(42) failure Result.Err("Not found") ``` ### Type-Level and Variant-Level Matching Use `match` to check variant types (preferred): ```hot describe-direction fn (dir): Str { match dir { Direction.Up => "going up" Direction.Down => "going down" Direction.Left => "going left" Direction.Right => "going right" } } describe-direction(Direction.Up) // "going up" describe-direction(Direction.Down) // "going down" ``` Use `is-type` for dynamic type checking: ```hot Coord type { x: Int, y: Int } c Coord({x: 1, y: 2}) is-type(c, Coord) // true is-type("hello", Coord) // false ``` ### Exhaustive Matching `match` on a closed enum must cover every variant or fall through to a `_` default arm. Union arms (`A | B`) count each variant they name, and an `Any` arm covers everything. The compiler reports `[non-exhaustive-match]` and lists the missing variants if none of these hold. ```hot Direction enum { Up, Down, Left, Right } // Exhaustive — every variant has an arm travel fn match (d: Direction): Str { Direction.Up => { "north" } Direction.Down => { "south" } Direction.Left => { "west" } Direction.Right => { "east" } } // Also exhaustive — the union arm covers Up and Down, `_` catches the rest classify fn match (d: Direction): Str { Direction.Up | Direction.Down => { "vertical" } _ => { "horizontal" } } ``` ### Open Enums Add the `open` modifier to declare an extensible enum. Other modules add new variants by declaring an arrow `Source -> Enum.Variant`. An open enum with no seed variants is a pure extension point: ```hot // Pure extension point — no initial variants Plugin enum open { } // Open enum with some seed variants Animal enum open { Dog, Cat } ``` Because the variant set of an open enum is unbounded, `match` on an open enum **must** include a `_` default arm — the compiler reports `[open-enum-match-missing-default]` otherwise. ```hot greet-animal fn match (a: Animal): Str { Animal.Dog => { "woof" } Animal.Cat => { "meow" } _ => { "hello, friend" } // required for open enums } ``` ### Arrow Enrollment A single arrow declaration both **enrolls a variant** in an open enum and **synthesizes its constructor**. The bodyless form is the idiomatic shorthand: ```hot Bird type { species: Str, wingspan: Dec } Bird -> Animal.Bird // bodyless — wraps the source value as-is eagle Animal.Bird({species: "Eagle", wingspan: 2.1}) ``` The variant name does not need to match the source type name. This is a **role-shaped variant**, useful when a domain-specific tag reads better than the data type name: ```hot Lizard type { length-cm: Dec } Lizard -> Animal.Reptile // tag is Reptile, payload is Lizard ``` Declaring the same arrow twice — even from different files — is a compile error (`[ambiguous-type-implementation]`) and the message points to both source spans. ## Type Coercion Define how types convert to each other using `->`: ```hot Date type { year: Int, month: Int, day: Int } // Define Date -> Str conversion (separate from type definition) Date -> Str fn (date: Date): Str { `${date.year}-${date.month}-${date.day}` } ``` ```result d Date({year: 2024, month: 12, day: 25}) Str(d) → "2024-12-25" ``` Multiple coercions: ```hot Temperature type { celsius: Dec } Temperature -> Str fn (temp: Temperature): Str { `${temp.celsius}°C` } Temperature -> Int fn (temp: Temperature): Int { round(temp.celsius) } temp Temperature({celsius: 23.7}) Str(temp) // "23.7°C" Int(temp) // 24 ``` Arrow declarations (`Source -> Target`) **must live at the top level of a namespace** (`[nested-type-implementation]`). Arrows mutate the global implementation registry, so a nested arrow inside a function body would be a hidden global side effect — another part of the program could pick up the implementation without ever seeing it referenced near where it was declared. Local **type** definitions inside function bodies are still allowed; the rule applies only to arrows. Arrow coercions are one-hop. Hot does not search a chain such as `A -> B -> C` to satisfy a parameter requiring `C`. If more than one direct arrow could satisfy the same conversion, the call is ambiguous rather than silently choosing one. ## Result Types Hot doesn't have exceptions. Instead, use `Result` for operations that can fail: ```hot // Create results explicitly success Result.Ok(42) failure Result.Err("Not found") // Using shorthand functions success ok(42) failure err("Not found") // Check results result safe-divide(10, 2) if(is-ok(result), `Result: ${result}`, `Error: ${result}`) ``` The `Result` type is an enum with `Ok` and `Err` variants: ```hot Result enum { Ok(Any), Err(Any) } success Result.Ok(42) `Value: ${success}` // "Value: 42" (auto-unwraps in templates) ``` Results automatically unwrap when used as function arguments—Ok values pass through, Err values halt execution. This makes error propagation seamless. See **[Error Handling](/docs/language/errors)** for the full story on Result types, automatic unwrapping, and lazy evaluation. ## Type Checking `hot check` validates annotations, known record fields, call signatures, exhaustive matches, and other statically visible constraints. It does not turn Hot into a fully static language: `Any`, dynamically shaped maps, and values from unresolved boundaries may defer failures until runtime. Structural compatibility and runtime identity answer different questions. A known record may satisfy a struct parameter by supplying every required field, while a value constructed with a type or enum constructor carries a nominal tag used by `match`, `is-type`, serialization, and overload dispatch. Use `is-*` functions to check built-in types at runtime: ```hot is-str(value) // true if Str is-int(value) // true if Int is-vec(value) // true if Vec is-map(value) // true if Map is-fn(value) // true if function is-null(value) // true if null is-some(value) // true if not null ``` For custom types, use `is-type`: ```hot Coord type { x: Int, y: Int } c Coord({x: 1, y: 2}) is-type(c, Coord) // true is-type("hello", Coord) // false ``` ## The `untype` Function Typed values carry internal metadata so Hot can preserve type identity at runtime. That wrapper is transparent to field access and is not a source-level reflection API: use normal field access, `match`, and `is-type`. `to-json` preserves that identity with Hot's tagged JSON representation, and `from-json` restores it. Treat the tag as a wire format rather than constructing or inspecting its metadata in Hot code. The serialized tag is an implementation boundary, not an alternate source-level type-construction or reflection API. Call `untype` before serialization when a recipient expects ordinary untagged JSON: ```hot // Define a type Person type { name: Str, age: Int } // Create a typed value alice Person({name: "Alice", age: 30}) // Produce an untagged value untype(alice) // {name: "Alice", age: 30} ``` ### When to Use `untype` The most common use case is serializing typed data to JSON for HTTP requests: ```hot // Tagged JSON is the default for typed values to-json(alice) // Untype explicitly requests ordinary untagged JSON to-json(untype(alice)) // {"name":"Alice","age":30} ``` Use `untype` when calling external APIs that expect untagged JSON payloads: ```hot // Sending typed data to an external API request-body ChatRequest({ model: "gpt-4", messages: [{role: "user", content: "Hello"}] }) // Untype before converting to JSON ::hot::http/request("POST", url, headers, to-json(untype(request-body))) ``` ### Recursive Untyping The `untype` function works recursively—it strips type metadata from nested types as well: ```hot Order type { customer: Person, items: Vec } order Order({ customer: Person({name: "Bob", age: 25}), items: [Item({name: "Widget", price: 9.99})] }) // Recursively removes all type metadata untype(order) // {customer: {name: "Bob", age: 25}, items: [{name: "Widget", price: 9.99}]} ``` ## Summary - Types are **optional** — add them where they help - **Types are constructors**: `Person({name: "Alice"})` creates a Person - Use `?` for optional/nullable types: `Str?` - Use `|` for union types: `Int | Str` - Use **literal unions** for exact value sets: `"apple" | "banana"` - Use **enums** (variant unions) for discriminated types: `Direction enum { Up, Down }` - Define type coercions with `Type -> OtherType fn` - Use `Result` with `Result.Ok()`/`Result.Err()` instead of exceptions (see [Error Handling](/docs/language/errors)) - `to-json` preserves typed values with tagged JSON; use `untype` when the target expects untagged JSON --- Source: https://hot.dev/docs/language/errors # Error Handling Operations that can fail return `Result` values. Hot makes working with Results ergonomic through **automatic wrapping**, **automatic unwrapping**, and **lazy argument evaluation**. ## The Result Type A `Result` represents either success (`Ok`) or failure (`Err`): ```hot Result enum { Ok(Any), Err(Any) } ``` **Return values are automatically wrapped** in `Result.Ok`, so you typically only need `err()` to signal failures: ```hot safe-divide fn (a: Int, b: Int): Int { if(eq(b, 0), err("Division by zero"), div(a, b)) // div result auto-wrapped in Ok } ``` Many core functions return Results implicitly—HTTP calls, file operations, parsing, and other fallible operations. ## Automatic Unwrapping When you use a `Result` value as a function argument or interpolate it in a template, Hot automatically handles it: - **Ok Result**: Unwraps to the inner value - **Err Result**: Immediately halts execution ```hot // HTTP functions return Results automatically response http-get("https://api.example.com/user/1") name response.body.name // Auto-unwraps the Result greeting `Hello, ${name}!` ``` If the HTTP call failed, execution halts at the point of use—you don't need explicit error handling on every line. Errors automatically propagate up. > **Note:** Function return type annotations specify the expected **success type**, not `Result`. The Result wrapper is implicit for any operation that can fail. ### Binding vs Consuming a Result Merely binding a `Result` preserves the tagged value. Propagation happens when ordinary code consumes it: ```hot result fetch-user(id) // Result remains intact in this binding valid is-ok(result) // inspected by a lazy Result-aware function page render(result) // Ok unwraps; Err halts and propagates here ``` Ordinary function arguments, template interpolation, and ordinary field or index access are consumption boundaries. `is-ok` and `is-err` inspect the wrapper without consuming it. `if-ok` and `if-err` pass the selected variant's unwrapped payload to a handler, then return a Result; the unmatched variant passes through unchanged. `match` also handles Result variants explicitly, as described in [Checking Results Explicitly](#checking-results-explicitly). ### Dot Access on Results Auto-unwrapping extends to field access. Dot access on an `Ok` Result reads fields from the **payload**, so you never need to unwrap before drilling in: ```hot response http-get("https://api.example.com/user/1") // returns a Result name response.body.name // reads .body.name from the Ok payload ``` If the Result is an `Err`, the dot access halts execution at that point — the same propagation rule as passing an Err to a function. Use `is-ok`, `is-err`, or `match` when code needs to inspect the Result without consuming it. ## Checking Results Explicitly Use `is-ok` and `is-err` to inspect Results without triggering automatic unwrapping: ```hot result-check safe-divide(10, 0) message-check if(is-ok(result-check), `Result: ${result-check}`, "Cannot divide by zero") // This branch runs ``` ```result safe-divide(10, 0) → Result.Err("Division by zero") message-check → "Cannot divide by zero" ``` These functions receive the Result as a **lazy argument**, which prevents automatic unwrapping during the check. You can also use `match` for pattern matching on Result variants: ```hot result safe-divide(20, 4) message match result { Result.Ok => `Success: ${result}` Result.Err => `Error: ${result}` } ``` The `match` chooses an arm using the `Result.Ok` or `Result.Err` tag. Within the selected arm, `result` evaluates to that variant's payload. ## Lazy Arguments and Result Checking When a function parameter is marked `lazy`, its argument expression is deferred until the function forces it with `do`. This is how Hot enables safe Result inspection. ```hot // The if function uses lazy arguments if fn cond (pred: Any, lazy then: Any, lazy else: Any): Any { pred => { do then } => { do else } } ``` When a lazy argument is forced, ordinary Result consumption remains **suppressed** inside that lazy context. This means: 1. The argument is not evaluated or consumed at the ordinary call boundary 2. `do` forces the expression inside the lazy context and preserves any Result 3. The function can inspect that Result without auto-unwrapping or propagating it ```hot // Safe division that returns a Result safe-divide fn (a, b) { if(eq(b, 0), err("Division by zero"), ok(div(a, b))) } // is-ok receives the Result without triggering auto-unwrap result safe-divide(10, 0) if(is-ok(result), `Result: ${result}`, "Cannot divide by zero") // This branch runs ``` ### Writing Your Own Result-Inspecting Functions The same rule applies to your own functions. A regular parameter auto-unwraps its argument, so passing an `Err` Result to it triggers the halt **before your function body runs**. Mark the parameter `lazy` and evaluate it with `do` — inside a lazy context, `do` preserves the Result instead of unwrapping it: ```hot // ❌ Regular parameter: an Err argument halts before the body executes describe fn (r: Any): Str { if(is-err(r), "failed", "succeeded") // never reached for Err values } describe(err("boom")) // Runtime error: boom // ✅ lazy parameter + do: receives and inspects the Result safely describe fn (lazy r: Any): Str { v do r if(is-err(v), "failed", "succeeded") } describe(err("boom")) // "failed" describe(ok(42)) // "succeeded" ``` This composes with `OnErr.Preserve` (see Pattern 5) to classify per-item outcomes without halting the batch: ```hot results map(items, process-item(%), OnErr.Preserve) // Errs stay as values labels map(results, describe(%)) // ["succeeded", "failed", ...] ``` If a halt fires when you hand a preserved `Result.Err` to a helper function, the fix is almost always marking the receiving parameter `lazy`. ## Short-Circuit Evaluation Lazy arguments also enable short-circuit evaluation for `and` and `or`: ```hot // Short-circuit prevents errors in unevaluated branches x-val 0 short-result if(eq(x-val, 0), "zero", div(10, x-val)) // div never called, no error ``` ## Error Handling Patterns ### Pattern 1: Let It Fail For many cases, just use Results directly. Errors propagate automatically: ```hot main fn () { user fetch-user(id) // Auto-unwraps or fails posts fetch-posts(user.id) // Auto-unwraps or fails render-page(user, posts) // Only runs if both succeeded } ``` ### Pattern 2: Check and Handle When you need to handle errors explicitly: ```hot result fetch-user(id) if(is-ok(result), render-profile(result), render-error-page(result)) ``` Or use `match` for cleaner syntax: ```hot result fetch-user(id) match result { Result.Ok => render-profile(result) Result.Err => render-error-page(result) } ``` ### Pattern 3: Default Values Provide fallbacks for failures: ```hot // Provide fallbacks for failures config-result safe-divide(10, 0) config if(is-ok(config-result), config-result, 99) // Fallback to 99 ``` ### Pattern 4: Fail on Broken Invariants Use `fail` to declare that the current run or task hit a bug or broken invariant and must stop. This is different from returning a normal domain `err(...)` value: expected failures — bad input, a refused connection, a query error — should be `err(...)` values the caller can branch on, while `fail()` and `cancel()` halt execution and surface at the run or task boundary (the `run:fail` event, or `status: "failed"` on the `TaskResult` returned by `::hot::task/await`). ```hot apply-migration fn (db, version: Int) { if(lt(version, current-version(db)), fail("migration version went backwards", {version: version}), run-migration(db, version)) } ``` ### Pattern 5: Preserve Domain Errors in Map-Shaped Calls Eligible higher-order functions force a normal `Result.Err` by default. Pass `OnErr.Preserve` when you intentionally want to keep per-item domain errors as values in the result: ```hot scores map([1, 0, 3], load-score, OnErr.Preserve) // keep Err slots as values failed filter(scores, is-err) // [Err("missing score")] ``` `OnErr` applies only to normal `err(...)` / `Result.Err(...)` values. It does not catch `fail()`, `cancel()`, or hard runtime errors. ### The Error Payload Convention Keep payloads in one of two shapes so error text survives every hop: - **Simple:** a plain `Str` — `err("connection refused")`. - **Structured:** a Map with a `message` field plus any structured fields — `err({message: "pg: relation missing", code: "42P01"})`. The auto-unwrap halt reads `message` (then `msg`) from Map payloads, and `err-message(result)` extracts readable text from any shape — including a halt's `Failure` payload — so handlers never hand-roll extraction: ```hot conn ::pg/connect(opts) if-err(conn, (e) { println(`db down: ${err-message(e)}`) }) ``` ### Pattern 6: Chain Fallible Steps `if-ok` flat-maps: the handler receives the Ok value, its return passes through unchanged, and an `Err` short-circuits out. Use it to chain steps where each depends on the previous one succeeding: ```hot greeting if-ok(open-conn("db.example"), (conn) { send-greeting(conn) }) // greeting = "greeted db.example" chain-err if-ok(open-conn("down.example"), (conn) { send-greeting(conn) // never runs; the Err short-circuits out }) // is-err(chain-err) = true ``` The first failing step becomes the whole chain's return value, as a single well-formed `Err`. ### Pattern 7: Supervise Untrusted Work with Tasks There is no `catch` in Hot. Code that must survive a `fail()` in work it does not control — arbitrary user callbacks, independent jobs — runs that work as a task. A halt inside the task never propagates to the caller; it surfaces as data on the awaited result: ```hot info ::hot::task/start(::myapp/risky-job, args) result ::hot::task/await(info.id) if(eq(result.status, "failed"), record-error("job", result.result), use-value(result.result)) ``` This server-side wait is appropriate because the same Hot execution consumes the result. When a client starts independent background work, return `info.id` instead and use the SDK task waiter. That waiter follows the durable task state without keeping the originating run alive. > **Note:** `::hot::lang/try` and `::hot::lang/try-call` were removed in Hot > 2.6.0. Old code that wrapped calls in `try` to detect failures should > branch on the returned `Result` directly (Pattern 2); fan-out loops that > used `try` for isolation should pass `OnErr.Preserve` (Pattern 5). ## Summary - Use `Result.Ok(value)` or `ok(value)` and `Result.Err(message)` or `err(message)` to create Results - Binding a Result preserves it; ordinary consumption triggers unwrapping or propagation - Results **auto-unwrap** when passed to functions or used in templates - Err Results **automatically fail** at point of use, carrying the payload's message—no explicit handling needed - Use `is-ok(result)` and `is-err(result)` to check without triggering auto-unwrap - Use `if-ok` to chain fallible steps; an `Err` short-circuits the chain - Use `OnErr.Preserve` with eligible map-shaped APIs when you intentionally want to keep domain errors as values - Use `fail()` / `cancel()` for bugs and broken invariants, not ordinary recoverable domain errors - Supervise untrusted work with a task boundary; await it in Hot only when the same execution needs the result - Return independent task ids to clients and wait with the SDK task resource - Use `match` for pattern matching on `Result.Ok` and `Result.Err` variants - Dot access on Results automatically accesses fields within the payload: `result.name` - **Lazy arguments** suppress Result checking, enabling safe inspection and short-circuit evaluation - Most code can ignore error handling; errors propagate automatically --- Source: https://hot.dev/docs/language/flows # Flows Flows are expressions that control how their contents execute and how their results are collected. Ordinary function bodies use a serial flow. Conditional, matching, and parallel flows make alternative execution strategies explicit. ## Flow Types | Flow | Description | |------|-------------| | `serial` | Execute sequentially (default) | | `parallel` | Execute concurrently | | `cond` | First matching branch wins | | `cond-all` | All matching branches execute | | `match` | Pattern match on types and values | | `match-all` | All matching type/value patterns execute | | `\|>` | Pipe data through transformations | ## Two Ways to Use Flows Every flow can be used in two ways: **1. With `fn`** — defines a function whose body uses that flow's scheduling and result rules: ```hot fetch-all-modifier fn parallel (id: Str): All { user api-get(`/users/${id}`) orders api-get(`/orders/${id}`) } ``` **2. As an inline expression** — provides local control flow: ```hot process-inline fn (id: Str): Map { // Inline parallel block data parallel { user api-get(`/users/${id}`) orders api-get(`/orders/${id}`) } // Inline conditional status cond { is-empty(data.orders) => { "new-customer" } => { "returning-customer" } } {data: data, status: status} } ``` The examples below show both approaches. ## Serial Flow (Default) Without a flow specifier, functions execute sequentially, returning the last value: ```hot process fn (x: Int): Int { doubled mul(x, 2) // First tripled mul(x, 3) // Second add(doubled, tripled) // Third - returned } ``` ```result process(5) → 25 ``` You can make it explicit with `serial`: ```hot process-explicit fn serial (x: Int): Int { doubled mul(x, 2) tripled mul(x, 3) add(doubled, tripled) } ``` ## Parallel Flow Request dependency-aware concurrency with `parallel`: ```hot fetch-all fn parallel (user-id: Str): All { user api-get(`/users/${user-id}`) orders api-get(`/orders/${user-id}`) preferences api-get(`/prefs/${user-id}`) } ``` ```result fetch-all("user-123") → {user: {...}, orders: {...}, preferences: {...}} ``` Parallelism is explicit: Hot never changes an ordinary serial body into a parallel one. Within a `parallel` flow, dependency scheduling is automatic. `parallel`, `cond-all`, and `match-all` naturally return all branch results. Use `All` or `All` when declaring that collected shape explicitly. A plain annotation such as `: Map` or `: Int` opts a naturally collect-all flow out of collection and describes its single final value instead. ### When to Use Parallel Use `parallel` when: - Operations involve I/O (HTTP, database, file system) - You want to speed up multiple slow operations Hot automatically analyzes dependencies and executes in "levels" - variables at the same level run concurrently, but levels execute in order: ```hot // Parallel with automatic dependency resolution enrich-user fn parallel (id: Str): All { user ::api/get-user(id) // Level 0 orders ::api/get-orders(user.id) // Level 1 (depends on user) prefs ::api/get-prefs(user.id) // Level 1 (depends on user) summary build-summary(orders, prefs) // Level 2 (depends on orders, prefs) } // user runs first, then orders+prefs run in parallel, then summary ``` Dependency analysis follows references between Hot bindings. It cannot infer conflicts in external state. Independent branches that write the same database row, file, or remote resource may race even when no Hot value connects them. If one branch fails, the flow fails. Sibling work that already started cannot be rolled back automatically, so independently scheduled side effects should be idempotent or explicitly coordinated. ## Conditional Flow Use `cond` for conditional branching. The first matching condition wins: ```hot classify fn cond (x: Int): Str { lt(x, 0) => { "negative" } eq(x, 0) => { "zero" } => { "positive" } } ``` ```result classify(-5) → "negative" classify(0) → "zero" classify(10) → "positive" ``` The `=>` arrow separates the condition from the result. A branch without a condition is the default case. Conditions are checked for **truthiness**: any value that isn't `false`, `null`, or an Err is considered true — including `0`, `""`, `[]`, and `{}`. The same rule backs `and`, `or`, `not`, and `is-truthy`; test emptiness explicitly with `is-empty`. This means you can use values directly as conditions: ```hot get-name fn cond (user: Map): Str { user.nickname => { user.nickname } // Truthy if nickname exists and isn't null user.name => { user.name } => { "Anonymous" } } ``` ```result get-name({nickname: "Bob", name: "Robert"}) → "Bob" get-name({name: "Alice"}) → "Alice" get-name({}) → "Anonymous" ``` ### Multiple Conditions ```hot grade fn cond (score: Int): Str { gte(score, 90) => { "A" } gte(score, 80) => { "B" } gte(score, 70) => { "C" } gte(score, 60) => { "D" } => { "F" } } ``` ```result grade(95) → "A" grade(75) → "C" grade(55) → "F" ``` ### Named Branches Give branches names for debugging or result identification: ```hot categorize fn cond (x: Int): Str { lt(x, 0) => negative { "negative" } eq(x, 0) => zero { "zero" } => positive { "positive" } } ``` ```result categorize(-5) → "negative" categorize(0) → "zero" categorize(5) → "positive" ``` ### Complex Conditions Any expression that returns a boolean works: ```hot validate fn cond (user: Map): Result { is-null(user.email) => { err("Email required") } not(valid-email(user.email)) => { err("Invalid email") } lt(length(user.password), 8) => { err("Password too short") } => { ok(user) } } ``` ## Conditional-All Flow Use `cond-all` when you want **all** matching branches to execute: ```hot apply-discounts fn cond-all (order: Map): All { order.is-member => member { "10% member discount" } gt(order.total, 100) => shipping { "Free shipping" } order.has-coupon => coupon { "Coupon applied" } => standard { "Standard pricing" } } ``` ```result apply-discounts({is-member: true, total: 150, has-coupon: true}) → {member: "10% member discount", shipping: "Free shipping", coupon: "Coupon applied"} ``` ### Use Cases for cond-all - Applying multiple rules/transformations - Collecting all matching categories - Running side effects for all matches - Validation that collects all errors ```hot validate-all fn cond-all (user: Map): All { is-null(user.name) => name { "Name required" } is-null(user.email) => email { "Email required" } lt(length(user.password), 8) => password { "Password too short" } // Returns ALL validation errors as a map, not just the first } ``` ```result validate-all({name: null, email: null, password: "short"}) → {name: "Name required", email: "Email required", password: "Password too short"} ``` ## Match Flow Use `match` to pattern match on types and literal values. The first matching pattern wins: ```hot Direction enum { Up, Down, Left, Right } ``` ```hot describe fn match (dir: Direction): Str { Direction.Up => "Going up" Direction.Down => "Going down" Direction.Left => "Going left" Direction.Right => "Going right" } up Direction.Up describe(up) // → "Going up" ``` ### Exhaustiveness A `match` on a closed `enum` must cover every variant or include a `_` / bare `=>` default arm. Missing variants produce **`non-exhaustive-match`** at compile time. Union arms (`A | B`) count every variant they name toward coverage, and an `Any` arm covers everything (it also satisfies the open-enum default requirement below). A `match` on an `open enum` MUST include a `_` / bare `=>` default arm, because additional variants can be enrolled later via `Source -> Enum.Variant` arrows. Missing the default produces **`open-enum-match-missing-default`**. ```hot Animal enum open { Dog, Cat } label fn match (a: Animal): Str { Animal.Dog => { "dog" } Animal.Cat => { "cat" } _ => { "other" } // required for open enums } ``` ### Value Matching Match against literal values — `Int`, `Dec`, `Str`, `Bool`, `Null`, `Vec`, `Map`: ```hot status-message fn match (code: Int): Str { 200 => { "ok" } 404 => { "not found" } 500 => { "server error" } => { "unknown" } } ``` ### Mixed Type and Value Arms Type and value arms can coexist. Arms are evaluated top-to-bottom; first match wins: ```hot describe fn match (value: Any): Str { null => { "null" } 0 => { "zero" } "" => { "empty string" } Int => { "integer" } Str => { "string" } => { "other" } } ``` ### Union Arms Combine several patterns in one arm with `|` — the arm matches if **any** atom matches. Atoms are the same pattern forms as single arms: types, enum variants, literal values, and fully qualified type paths (`::hot::type/Str`), mixed freely: ```hot describe fn match (value: Any): Str { "" | Null => { "blank" } Int | Dec => { "number" } "yes" | "y" | true => { "affirmative" } => { "other" } } ``` Enum variants union the same way, and union arms count toward exhaustiveness — this match is exhaustive without a default arm: ```hot Shape enum { Circle, Square, Triangle } classify fn match (s: Shape): Str { Shape.Circle | Shape.Square => { "round-ish" } Shape.Triangle => { "pointy" } } ``` Bindings receive the matched value as usual: `Str | Null (v) => { ... }`. ### Optional-Type Sugar `T?` in a match arm means `T | Null`, exactly as in signatures: ```hot greet fn (name: Str?): Str { match name { Str? => { `Hello, ${or(name, "stranger")}` } // same as Str | Null _ => { "Hello, whatever you are" } } } ``` ### The Any Pattern `Any` is the top type: it matches every value. An `Any` arm acts like a default arm (and satisfies exhaustiveness), but unlike `_` it can carry a binding: ```hot kind match value { Str => { "string" } Any (v) => { `something else: ${v}` } } ``` ### Expression Subjects The match subject can be any expression — it is evaluated once: ```hot result match length(name) { 0 => { "empty" } 5 => { "five chars" } => { "other" } } ``` ### Vec and Map Arms Match collections by full structural equality: ```hot result match coords { [0, 0] => { "origin" } [1, 0] => { "unit x" } => { "other" } } ``` ### Inline Match Use `match` inline to branch on a value: ```hot result get-result() message match result { Result.Ok => `Success: ${result}` Result.Err => `Error: ${result}` } ``` ### Type-Level Matching Match any variant of a type: ```hot // Matches any Result variant is-result match value { Result => true => false } ``` ### Match Functions with Extra Arguments Match flow functions can have additional arguments beyond the matched value: ```hot Direction enum { Up, Down, Left, Right } ``` ```hot describe-direction fn match (dir, prefix: Str): Str { Direction.Up => concat(prefix, " going up") Direction.Down => concat(prefix, " going down") Direction.Left => concat(prefix, " going left") Direction.Right => concat(prefix, " going right") } ``` ```result describe-direction(Direction.Up, "We are") → "We are going up" ``` ## Match-All Flow Use `match-all` when you want **all** matching patterns to execute: ```hot Trait enum { Flying, Swimming, Walking } ``` ```hot describe-traits fn match-all (trait): All { Trait.Flying => "Can fly" Trait.Swimming => "Can swim" Trait.Walking => "Can walk" } ``` ```result describe-traits(Trait.Flying) → {"Trait.Flying": "Can fly"} ``` Results are keyed by the arm's pattern; a union arm produces a single key joining its atoms (e.g. `"Int | Dec"`). ### Match Result Shape Like other flows, match supports `All` annotations to collect branch results. Use plain return types for single values and `All` / `All` for collected results. Bare `All` is allowed only where the language already has a natural collect-all default: `parallel`, `cond-all`, and `match-all`. On `serial`, `pipe`, `cond`, and `match`, use explicit `All` or `All` to make the collection shape clear. ```hot // match defaults to one winning result // match-all defaults to All (keyed by branch) // Get results as vector traits: All match-all creature { Trait.Flying => "flies" Trait.Swimming => "swims" } ``` ## Pipe Flow The pipe `|>` chains transformations. The piped value becomes the **first argument** of the next function: ```hot result 5 |> add(2) |> mul(3) // 5 |> add(2) → add(5, 2) → 7 // 7 |> mul(3) → mul(7, 3) → 21 ``` ### Collection Pipelines Pipes shine with collection operations: ```hot // Using % placeholder lambdas for concise single-param operations result [1, 2, 3, 4, 5] |> map(mul(%, 2)) // [2, 4, 6, 8, 10] |> filter(gt(%, 5)) // [6, 8, 10] |> reduce((a, x) { add(a, x) }, 0) // 24 (multi-param: use explicit lambda) ``` ### Pipes and `%` — How They Compose Two rules govern how pipes and `%` interact: 1. **The pipe supplies the piped value as the first argument.** Don't add `%` for that — a pipe stage is already a partial call: ```hot result 10 |> mul(2) // mul(10, 2) → 20 |> add(5) // add(20, 5) → 25 ``` 2. **`%` creates a lambda only inside an argument that expects a function** — the higher-order arguments of `map`, `filter`, `reduce`, and friends: ```hot [1, 2, 3] |> map(mul(%, 2)) // % is each element → [2, 4, 6] ``` A bare `%` in a pipe stage that isn't a function-typed argument is a compile error: ```hot 10 |> mul(%, 2) // error: Placeholder `%` has no enclosing parameter slot of type `Fn` // to bind to. The pipe already passes 10 as the first argument — write // `10 |> mul(2)` instead. ``` When you need a lambda where Hot wouldn't create one automatically, mark the boundary explicitly with `%(expr)` — see [Explicit Lambda Boundary](/docs/language/functions#explicit-lambda-boundary). ### Real-World Pipeline ```hot process-users fn (users: Vec): Vec { users |> filter(%.active) |> map(%.email) |> filter(ends-with(%, "@company.com")) |> map(lowercase(%)) } ``` ## Combining Flows Use flows within function bodies: ```hot process-order fn (order: Map): Result { // Validate first (conditional) validation cond { is-null(order.items) => { err("No items") } eq(length(order.items), 0) => { err("Empty order") } => { ok(order) } } // Then enrich in parallel (returns a map) enriched parallel { customer fetch-customer(order.customer-id) inventory check-inventory(order.items) shipping calculate-shipping(order) } // Return combined result (access via enriched.*) ok({ order: order, customer: enriched.customer, inventory: enriched.inventory, shipping: enriched.shipping }) } ``` ## Flow vs Function Flows are expressions. Combining `fn` with a flow creates a callable function whose body uses that flow's scheduling and result rules: ```hot // Function with conditional flow classify fn cond (x: Int): Str { lt(x, 0) => { "negative" } => { "positive" } } // Inline flow expression inside a function body process fn (data: Map): Result { result cond { is-null(data) => { err("No data") } => { ok(data) } } result } ``` ## Flow Result Shape Flow result shape controls whether a flow returns its single produced value or all produced values. Use a plain type annotation for the single value case and `All` / `All` when you want a collected result: ```hot // Single value (the default for serial, cond, match, and pipe) result: Int serial { a 1 b 2 } // All values as a vector values: All serial { a 1 b 2 } // All values as a map keyed by branch or variable name data: All parallel { user ::api/get-user(id) orders ::api/get-orders(id) } // Any other type opts a collect-all flow OUT of collection: the // annotation states the type of the single final value. On // single-value flows a plain annotation is an ordinary type check. last: Int parallel { a 1 b 2 } ``` Bare `All` is accepted only on natural collect-all flows (`parallel`, `cond-all`, and `match-all`). Use `All` or `All` on other flows. Annotations are not enforced at runtime, but `hot check` reports an `annotation-mismatch` warning when an annotation names a type the value can never be (for example `x: Int parallel { ... }` whose final value is a `Str`). ### Default Flow Shapes Each flow type has a sensible default: | Flow | Default | Behavior | |------|---------|----------| | `serial` | Single value | Returns the last expression's value | | `parallel` | `All` | Returns all results as a map keyed by variable name | | `cond` | Single value | Returns the matching branch's value | | `cond-all` | `All` | Returns all matching results as a map keyed by branch name | | `match` | Single value | Returns the matching arm's value | | `match-all` | `All` | Returns all matching results as a map keyed by pattern | | `\|>` (pipe) | Single value | Returns the final piped value | ### Explicit Result Shapes Override the default when you need different results: ```hot // Parallel defaults to All data parallel { user ::api/get-user(id) orders ::api/get-orders(id) prefs ::api/get-prefs(id) } // => {user: ..., orders: ..., prefs: ...} // Bare All is accepted on collect-all flows and keeps the natural map shape data: All parallel { user ::api/get-user(id) orders ::api/get-orders(id) } // => {user: ..., orders: ...} // Parallel with All - get results as a vector values: All parallel { a fetch-a() b fetch-b() c fetch-c() } // => [, , ] // cond-all defaults to All results cond-all { check-a() => a { "A passed" } check-b() => b { "B passed" } check-c() => c { "C passed" } } // => {a: "A passed", c: "C passed"} (if A and C pass) // cond-all with All - collect as vector (no branch names) discounts: All cond-all { is-member => { "10% off" } gt(total, 100) => { "Free shipping" } has-coupon => { "Coupon applied" } } // => ["10% off", "Free shipping"] (if member with $150 order, no coupon) // Pipe with All - collect all intermediate values steps: All 5 |> add(2) |> mul(3) // => [5, 7, 21] ``` Parallel scheduling is defined by named bindings and their dependencies. Standalone, unbound expressions do not become collected result slots; bind every concurrent operation even when using `All`. A `fn parallel` definition can collect unbound body expressions into an explicit `All`, but named bindings work consistently in both forms. Branches complete independently, so an `Err` remains in its branch's slot rather than cancelling siblings. It propagates when ordinary code later consumes that result. Because branches are independent slots, a deep-path assignment into an existing binding (`st.a.b 99`) is rejected at compile time inside `parallel` — both the standalone block and `fn parallel` forms: concurrent writes into a shared root have no defined order. Bind a new name in the branch and merge after the flow, or use a serial flow. The check does not look inside nested flows: a `serial { }` block nested in a parallel branch can still write to an outer binding, but its merge order across branches is undefined — treat that pattern as unsupported. ## Summary | Flow | Use When | |------|----------| | `serial` | Sequential execution (default) | | `parallel` | Concurrent execution with automatic dependency resolution | | `cond` | Choose one branch based on conditions | | `cond-all` | Execute all matching branches | | `match` | Pattern match on types and values | | `match-all` | Execute all matching type/value patterns | | `\|>` | Chain transformations on data | Flows make Hot's scheduling and result behavior explicit. You always know whether operations run in sequence, parallel, or conditionally. --- Source: https://hot.dev/docs/language/not-supported # What Hot Doesn't Have Hot intentionally omits certain syntax found in other languages. This isn't a limitation—it's a design choice that keeps the language simple and consistent. ## No Infix Operators Hot has no `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `>`, `&&`, `||`, etc. | Instead of | Use | |------------|-----| | `a + b` | `add(a, b)` | | `a - b` | `sub(a, b)` | | `a * b` | `mul(a, b)` | | `a / b` | `div(a, b)` | | `a % b` | `mod(a, b)` | | `a == b` | `eq(a, b)` | | `a != b` | `ne(a, b)` | | `a < b` | `lt(a, b)` | | `a > b` | `gt(a, b)` | | `a <= b` | `lte(a, b)` | | `a >= b` | `gte(a, b)` | | `a && b` | `and(a, b)` | | `a \|\| b` | `or(a, b)` | | `!a` | `not(a)` | **Why?** Consistency. Everything is a function call, with predictable evaluation order and no operator precedence to remember. > **Watch out:** `%` is not the modulo operator in Hot — it's the [lambda placeholder](/docs/language/functions#placeholder-lambdas). Use `mod(a, b)` for remainders. ## No Assignment Operator Hot has no `=` for assignment. | Instead of | Use | |------------|-----| | `name = "Alice"` | `name "Alice"` | | `count = 42` | `count 42` | Variables are declared by placing the name before the value. ## No If/Else Blocks Hot has no `if`/`else` statement syntax. | Instead of | Use | |------------|-----| | `if (x) { a } else { b }` | `if(x, a, b)` | | `if (x) { a }` | `if(x, a)` | Or use `cond` for multiple conditions: ```hot cond { lt(x, 0) => "negative" eq(x, 0) => "zero" => "positive" } ``` ## No Loops Hot has no `for`, `while`, `do-while`, or any loop constructs. | Instead of | Use | |------------|-----| | `for (x of items)` | `map(items, (x) { ... })` | | `items.filter(...)` | `filter(items, (x) { ... })` | | `items.reduce(...)` | `reduce(items, (acc, x) { ... }, init)` | | `items.forEach(...)` | `for-each(items, (x) { ... })` | | `while (cond) { }` | Tail-recursive function | **Why?** Loops imply mutation. Functional transformations are clearer and parallelize better. ### Tail Call Optimization (TCO) Hot has automatic TCO for tail-recursive functions. Use the accumulator pattern for custom iteration: ```hot // Tail-recursive - stack-safe for any list size sum-list fn cond (xs: Vec, acc: Int): Int { is-empty(xs) => { acc } => { sum-list(rest(xs), add(acc, first(xs))) } } sum-list([1, 2, 3, 4, 5], 0) // 15 ``` This works for arbitrarily large collections without stack overflow. ## No Classes or Interfaces Hot has no `class`, `interface`, `extends`, or `implements`. | Instead of | Use | |------------|-----| | `class User { }` | `User type { name: Str }` | | `new User()` | `User({name: "Alice"})` | | `interface Printable` | Type coercion: `Type -> Str` | | `extends BaseClass` | Composition | Types in Hot are data definitions, not behavior containers. Add behavior with functions: ```hot User type { name: Str, email: Str } // Functions that work on User greet-user fn (user: User): Str { `Hello, ${user.name}!` } // Type coercion for "interface-like" behavior User -> Str fn (user: User): Str { `${user.name} <${user.email}>` } ``` ## No Exceptions Hot has no `throw`, `catch`, `finally`, or `try { ... } catch { ... }` blocks — and no `try(...)` function either. Expected failures are `Result.Err` values you branch on; `fail()` signals a broken invariant and halts the run or task (there is no catch). To supervise work that may halt, run it behind a task boundary (`::hot::task/start` + `await`) — see [Error Handling](/docs/language/errors). | Instead of | Use | |------------|-----| | `throw new Error("msg")` | `err("msg")` or `Result.Err("msg")` | | `try { } catch { }` | `if(is-ok(result), ..., ...)` or `match` | Use `Result` types for error handling: ```hot safe-divide fn (a: Int, b: Int): Int { if(eq(b, 0), err("Division by zero"), div(a, b)) } result safe-divide(10, 0) // Use match for pattern matching on Result match result { Result.Ok => println(`Result: ${result}`) Result.Err => println(`Error: ${result}`) } ``` ## No Mutable Variables Hot has no `let`, `var`, or reassignment. ```hot count 1 count 2 // Creates a NEW binding, shadows the first ``` This isn't mutation—it's creating a new variable that shadows the old one. For accumulating values, use `reduce`: ```hot // Instead of: let sum = 0; for (x of items) { sum += x; } total reduce(items, (sum, x) { add(sum, x) }, 0) ``` ## No Return Statement The last expression in a function body is the return value: ```hot sum fn (a: Int, b: Int): Int { add(a, b) // This is returned } ``` No `return` keyword exists. ## No Ternary Operator Hot has no `? :` ternary. | Instead of | Use | |------------|-----| | `x ? a : b` | `if(x, a, b)` | ## Summary: The Hot Way | Concept | Other Languages | Hot | |---------|----------------|-----| | Math | `a + b * c` | `add(a, mul(b, c))` | | Comparison | `a == b && c > d` | `and(eq(a, b), gt(c, d))` | | Conditionals | `if/else` blocks | `if()` function or `cond` | | Loops | `for`, `while` | `map`, `filter`, `reduce` | | Objects | `class` + `new` | `type` + constructor | | Errors | `throw`/`catch` | `Result.Err()`/`match` | | Mutation | `x = x + 1` | `new-x add(x, 1)` | The tradeoff: Hot code looks different from JavaScript/Python/etc. The benefit: Complete consistency, easier parallelization, and no hidden complexity. --- Source: https://hot.dev/docs/platform # Hot Platform The Hot Platform is a complete backend workflow automation system. It combines a purpose-built programming language with managed infrastructure for running, monitoring, and scaling your workflows. Start with the [Platform Execution Model](/docs/platform/execution-model) to see how events, handlers, runs, tasks, retries, workers, and streams fit together. ## Architecture Overview Your Backend Web Server CLI Scripts Integrations Hot Scheduler Cron & scheduled jobs REST calls & SSE subscriptions cron fires → emits events Hot API Execute functions, send events, manage files publishes events Streams Group related events, runs & tasks for end-to-end tracing Events — async triggers Runs & tasks — executions workers route events & execute runs and tasks Hot Workers Worker Worker Worker Scale horizontally — each worker executes runs in isolation events, runs, tasks & streams recorded Hot App Monitor runs, inspect events, debug workflows ## Core Concepts ### Runs A run is one tracked, top-level function execution attempt initiated by the platform. Function calls inside that execution remain part of its trace rather than creating additional runs. Each run has: - Unique run ID for tracking - Full execution trace - Input parameters and return values - Timing and performance data - Error details if failed [Learn more about Runs →](/docs/platform/runs-events-streams) ### Events Events are the primary way to trigger asynchronous workflows. Emit events from your application or external systems, and Hot automatically routes them to registered handlers. ```hot // Define an event handler on-user-signup meta {on-event: "user:created"} fn (event) { send-welcome-email(event.data.email) create-default-settings(event.data.id) } ``` [Learn more about Events →](/docs/platform/runs-events-streams) ### Streams Streams correlate related events, runs, retries, and tasks into one execution history. The same stream ID also supports live run notifications and user-emitted data for AI responses, progress updates, and other interactive work. [Learn more about Streams →](/docs/platform/runs-events-streams) ### Tasks Tasks are long-running asynchronous resources started by runs or other tasks. They inherit the current stream and are linked to the run that started them. Task execution is also represented by a task-type run. [Learn more about Tasks →](/docs/tasks) ### Workers Workers are the execution engine of the Hot Platform. They consume queued events and tasks, execute Hot code or containers, and report results back. - Scale horizontally by adding more workers - Process event handlers and scheduled jobs - Execute in isolated contexts for security [Learn more about Workers →](/docs/events) ## Platform Components | Component | Purpose | |-----------|---------| | [Hot API](/docs/api) | API for executing functions, sending events, and managing files | | [Workers](/docs/events) | Execution engine for running Hot code | | [Alerts](/docs/alerts) | Monitor your applications with notifications for run failures, deployments, and custom events | | [MCP Services](/docs/mcp) | Expose Hot functions as MCP tools for AI agents | | [Webhooks](/docs/webhooks) | Turn Hot functions into webhook endpoints for external services | | [Custom Domains](/docs/domains) | Map your own domain names to your Hot Dev environment | | [Hot App](/docs/app) | Real-time monitoring and debugging interface | ## Deployment Options ### Hot Cloud (Managed) Deploy to the Hot Cloud with a single command: ```bash hot deploy ``` Hot Cloud provides: - Managed workers with auto-scaling - Global edge deployment - Built-in observability - Zero infrastructure management [See pricing →](/pricing) --- Source: https://hot.dev/docs/platform/execution-model # Platform Execution Model The platform execution model describes how Hot creates and connects durable execution units. It is distinct from the [Language Evaluation Model](/docs/language/execution-model), which describes how expressions, arguments, flows, and Results behave inside one run or task attempt. ## The Lifecycle Most platform work follows this pattern:
Hot Platform execution lifecycle An external trigger creates a persisted event. The event routes to matching handlers, each selected handler gets a run, and that run can emit a child event or start a task in the same stream. API call, webhook, schedule, or send platform entry point STREAM · SHARED EXECUTION LINEAGE Persisted event creates or continues the stream Handler routing zero to many matching handlers Handler run attempt one run per selected handler send(...) task/start Child event inherits the stream ID Task resource same stream · linked origin run Downstream run(s) Task execution run
A **stream** ties the whole chain together. Events, runs, retries, and tasks carry the same stream ID as work continues. A new externally published event creates a stream unless the caller supplies an existing `stream_id`. ## Platform Units ### Event An event is a persisted message. The platform routes it to matching handler definitions. One event may select zero, one, or multiple handlers; each selected handler invocation receives its own run attempt. Events use at-least-once delivery. Redelivery and retries can produce more than one attempt, and events in the same stream are not guaranteed to execute in strict order. ### Handler A handler is a function definition registered for an event type. It is routing metadata, not a separate execution record. The execution of a selected handler is recorded as a run. ### Run A run is one platform-invoked, top-level function execution attempt. API calls, event handlers, schedules, and task workers can create runs. Ordinary Hot function calls made inside that top-level function do **not** create additional platform runs. They appear as calls within the current run's execution trace. Publishing an event, starting a task, or retrying work crosses a platform execution boundary and creates a new linked unit. A retry is a new run attempt linked to its prior run through `origin_run_id`. It keeps the same triggering data and stream. ### Task A task is a long-running asynchronous resource started from a run or another task. It inherits the current stream and records the run that started it. Executing the task also creates a task-type run, so its function calls, result, timing, and failures remain observable through the same run model. Code tasks add messaging and checkpoints. Container tasks run an OCI container. See [Tasks](/docs/tasks) for their lifecycles and APIs. ### Stream A stream has two related roles: 1. **Execution lineage** — it correlates the events, runs, retries, and tasks that belong to one workflow or interaction. 2. **Live delivery** — clients can subscribe to run lifecycle notifications and data emitted with `::hot::stream/data`. Events, runs, and task records are durable. User-emitted `stream:data` messages are live delivery payloads and are not persisted as workflow records. A stream is also not a serialization lock: workers may process related work concurrently. ### Workers Workers consume queued events and tasks. Event workers route events and execute selected handlers as runs; task workers execute code or container tasks. Worker scaling changes throughput, not the lineage relationships recorded in the stream. ## What Creates a New Unit? | Operation | Platform effect | |-----------|-----------------| | Call a Hot function normally | Stays inside the current run or task trace | | `send(...)` | Persists a child event in the current stream | | `send("hot:call", ...)` | Persists an event that dispatches another function as a new run | | `::hot::task/start(...)` or `::hot::box/start(...)` | Creates a task linked to the current run and stream | | Retry a failed run | Creates a new run linked to the prior attempt | | `::hot::stream/data(...)` | Publishes ephemeral live data on the current stream | ## Persistence and Lineage | Record | Key relationships | |--------|-------------------| | Event | `event_id`, `stream_id` | | Run | `run_id`, `event_id`, `stream_id`, optional `origin_run_id` | | Task | `task_id`, `stream_id`, `origin_run_id`, associated execution `run_id` | | Stream | Correlation ID and aggregate history for the workflow | For detailed state machines and APIs, continue with [Runs, Events & Streams](/docs/platform/runs-events-streams), [Tasks](/docs/tasks), and [Durable Execution](/docs/platform/durability). --- Source: https://hot.dev/docs/platform/runs-events-streams # Runs, Events & Streams This page is the detailed reference for run and event records plus stream lineage and live delivery. Start with the [Platform Execution Model](/docs/platform/execution-model) for the complete relationship among events, handlers, runs, tasks, retries, workers, and streams. ## Runs A **Run** is one platform-invoked, top-level Hot function execution attempt, such as an API call, selected event handler, schedule, or task execution. Ordinary function calls inside it remain part of the same run's execution trace. ### Run Lifecycle running succeeded failed cancelled retry left? pending_retry retries as a new run (linked via origin_run_id) | State | Description | |-------|-------------| | `running` | Worker is executing the function | | `succeeded` | Function completed successfully | | `failed` | Function threw an error or timed out | | `cancelled` | Run was cancelled before completion | | `pending_retry` | Function failed but will be retried automatically | Runs with `"retry"` metadata that fail are temporarily set to `pending_retry` until the retry executes. See [Retries](/docs/retries) for details. ### Run Data Every run captures: ```json { "run_id": "run_abc123xyz", "function": "::myapp::orders/process-order", "status": "succeeded", "input": { "order_id": "ord_12345" }, "result": { "status": "processed", "total": 99.99 }, "started_at": "2024-12-04T10:30:00Z", "completed_at": "2024-12-04T10:30:02Z", "duration_ms": 2150, "trigger": { "type": "event", "event_id": "evt_xyz789" } } ``` ### Execution Trace Hot captures a full execution trace for every run, showing: - Each expression evaluated - Intermediate values - Function calls and returns - Timing for each step - Any errors with stack traces ### Triggering Runs Runs can be triggered in several ways: **1. API Call** (via `hot:call` event) ```bash curl -X POST https://api.hot.dev/v1/events \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_type": "hot:call", "event_data": {"fn": "::myapp::orders/process-order", "args": [{"order_id": "12345"}]}}' ``` **2. Event Handler** ```hot on-order-created meta {on-event: "order:created"} fn (event) { process-order(event.data.order_id) } ``` **3. Schedule** (recurring) ```hot daily-report meta {schedule: "0 0 * * *"} fn (event) { generate-report() } ``` **4. Dynamic Schedule** (one-time or created at runtime) ```hot // Schedule a function to run in 10 minutes send("hot:schedule:new", { fn: "::myapp::tasks/process", args: [{task_id: "123"}], schedule: "in 10 minutes" }) ``` See [Dynamic Schedules](/docs/schedules#dynamic-schedules) for more details. **5. Asynchronous Function Dispatch** (from another run) ```hot process-batch fn (orders) { // Each send publishes an event that dispatches a separate run map(orders, (order) { send("hot:call", { fn: "::myapp::orders/process-order", args: [order] }) }) } ``` ## Events **Events** are messages that trigger asynchronous workflows. They decouple event producers from consumers, enabling scalable and maintainable systems. ### Event Structure An Event in Hot has two fields: ```hot Event type { type: Str, data: Any } ``` The `send` function has two arities: ```hot // Pass event type and data directly send("user:created", {id: "usr_12345", email: "alice@example.com"}) // Or pass an Event send(Event({type: "user:created", data: {id: "usr_12345", email: "alice@example.com"}})) ``` ### Sending Events **From Hot Code:** ```hot // Send an event after user creation create-user fn (data) { user insert-user(data) // Send event for other handlers (send is a core function) send("user:created", { id: user.id, email: user.email, name: user.name }) user } ``` **From the API:** ```bash curl -X POST https://api.hot.dev/v1/events \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "user:created", "event_data": {"id": "usr_12345", "email": "alice@example.com"} }' ``` **From External Systems (Webhooks):** Configure webhooks to forward events from services like Stripe, GitHub, or Slack directly to Hot. ### Event Handlers Define handlers using the `on-event` metadata: ```hot ::myapp::notifications ns // Handle a specific event type on-user-created meta {on-event: "user:created"} fn (event) { send-welcome-email(event.data.email) } ``` An event may match zero, one, or multiple handlers. Each selected handler invocation is recorded as its own run in the event's stream. ### Event Delivery Hot guarantees **at-least-once delivery** for events: - Events are persisted before acknowledgment - Failed handlers can be [retried automatically](/docs/retries) with configurable attempts and delay - Retry status is visible in the Hot App UI Delivery is optimized for durability and throughput, not strict global ordering. Concurrent workers may process different events from the same stream at the same time, and retries or infrastructure redelivery can arrive after newer events. Queue message fields are additive so rolling deploys can read older messages; workers hydrate the authoritative event payload from the database before routing. Handlers should be idempotent when they perform external side effects, because the same event can be delivered more than once after a retry, worker crash, Redis pending-entry reclaim, or task reconciliation pass. This also applies to run timeouts: when a handler exceeds its run timeout it is recorded as a failure and retried according to its retry policy. The worker cancels the timed-out run cooperatively, but a handler stuck in non-cooperative work (a tight native loop or blocking syscall) can keep running in the background while its retry begins, so the two attempts may briefly overlap. ## Streams **Streams** are the correlation boundary for platform work. Related events, runs, retries, and tasks share a stream ID, producing one end-to-end workflow history. A new externally published event creates a stream unless its request supplies an existing `stream_id`; events and tasks created inside running Hot code inherit the current stream. The same stream ID is also a live delivery channel. Clients can subscribe to run lifecycle notifications and data emitted with `::hot::stream/data`. User-emitted stream data is ephemeral and is not persisted with the durable event, run, and task records. ### Use Cases - **Workflow lineage** - Trace related events, runs, retries, and tasks - **AI/LLM Responses** - Stream tokens as they're generated - **Live Updates** - Push data to clients in real-time - **Long-Running Operations** - Report progress incrementally - **Bidirectional Communication** - WebSocket-style interactions ### Server-Sent Events (SSE) Stream data to clients in real-time using `::hot::stream/data`. **Hot code** — emit chunks as they arrive: ```hot handle-chat meta { on-event: "chat:message" } fn (event) { // Call a streaming AI API response ::anthropic::messages/post-stream({ model: "claude-sonnet-4-20250514", max_tokens: 4096, messages: [{role: "user", content: event.data.message}] }) // Process stream and emit chunks to the client process-stream(response.body, "") } // Recursive stream processor process-stream fn (iter, accumulated: Str): Str { result next(iter) cond { result.done => { accumulated } => { delta or(result.value.data.delta.text, "") // Emit chunk to client in real-time ::hot::stream/data("ai:delta", { text: delta }) process-stream(iter, concat(accumulated, delta)) } } } ``` **JavaScript client** — publish an event, then subscribe to the stream: ```javascript // 1. Publish event to trigger the handler const eventRes = await fetch('/v1/events', { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ event_type: 'chat:message', event_data: { message: 'Hello!' } }) }); const { data: { stream_id } } = await eventRes.json(); // 2. Subscribe to stream for real-time updates // GET (classic SSE) and POST (streamable HTTP style) are both supported. const response = await fetch(`/v1/streams/${stream_id}/subscribe`, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Accept': 'text/event-stream' } }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // Parse SSE events (data: {...}\n\n format) for (const line of text.split('\n')) { if (line.startsWith('data: ')) { const event = JSON.parse(line.slice(6)); if (event.type === 'stream:data') { // Real-time chunk from ::hot::stream/data appendToResponse(event.payload.text); } if (event.type === 'run:stop') { // Run completed console.log('Final result:', event.run.result); } } } } ``` ### Subscription States
Live stream subscription lifecycle An open subscription becomes active, then either closes normally or enters an error state. open connection established subscribed active receiving live events complete failure closed normal completion error connection interrupted
The subscription lifecycle is distinct from the durable stream record. Closing a client connection does not delete the events, runs, or tasks correlated by the stream ID. ### Viewing Streams Active and completed streams are visible in the Hot App: - Connection status and duration - Messages sent/received - Bandwidth usage - Error details ## Monitoring All runs, events, tasks, and streams are visible in the **Hot App** with: - Real-time updates as executions happen - Filtering by status, function, event type - Full-text search across payloads - Detailed drill-down views --- Source: https://hot.dev/docs/platform/durability # Durable Execution Hot provides durable execution out of the box. Every platform-invoked, top-level function execution is a persisted **run** with recorded inputs, outputs, and a full execution trace. Nested function calls appear inside that trace. Multi-step workflows are chains of **events** and runs—each step is independently tracked, retryable, and recoverable. See the [Platform Execution Model](/docs/platform/execution-model) for how events, handlers, runs, tasks, retries, workers, and streams connect before diving into the durability guarantees below. ## How It Works The durability model is built on three ideas: 1. **Runs are atomic, durable steps.** Each run takes input, executes, and produces a result. All of this is persisted — if a run fails, the platform knows exactly what happened and can retry it. 2. **Events are the workflow journal.** A run can emit events that trigger downstream runs. The chain of events connecting runs is itself persisted, forming a complete record of your workflow's progress. 3. **Retries use the original input.** When a run fails and is retried, the retry receives the same event data as the original. No partial state to reconstruct — each attempt is a clean execution with the same input.
Durable event and run retry chain Event A triggers Run 1, which succeeds and emits Event B. Run 2 fails, then a new linked retry uses the same Event B data, succeeds, emits Event C, and triggers Run 3. ONE STREAM · EVERY STEP PERSISTED Event A workflow input Run 1 succeeded emits Event B next-step input Run 2 failed new attempt · same Event B data · linked origin_run_id Run 2 retry succeeded emits Event C workflow continues Run 3 next durable step
Every arrow in this chain is persisted. Every run captures its input, output, timing, and full execution trace. If anything fails, retries pick up from the failed step — not from the beginning of the workflow. ## What Gets Persisted Every run automatically captures: - **Input** — the event data or arguments that triggered it - **Result** — the return value on success, or error details on failure - **Execution trace** — every function call, intermediate value, and timing - **Status** — `running`, `succeeded`, `failed`, `cancelled`, or `pending_retry` - **Lineage** — which event triggered this run, and which run emitted that event This happens without any additional code. Write a function, attach it to an event or schedule, and the platform handles the rest. ```hot on-order-created meta {on-event: "order:created", retry: 3} fn (event) { validated validate-order(event.data) charge-payment(validated) send("order:confirmed", {order-id: validated.id}) } ``` If `charge-payment` fails on the first attempt, the run is marked `pending_retry` and re-executed with the same `order:created` event data. The retry is linked to the original via `origin_run_id`, so you can trace the full history. On success, the `order:confirmed` event triggers the next step in the workflow. ## Multi-Step Workflows Complex workflows are composed as chains of events and handlers. Each handler is a durable step: ```hot ::myapp::orders ns // Step 1: Validate and charge on-order-created meta {on-event: "order:created", retry: 3} fn (event) { order validate-order(event.data) charge-payment(order) send("order:paid", {order-id: order.id, amount: order.total}) } // Step 2: Fulfill on-order-paid meta {on-event: "order:paid", retry: 5} fn (event) { shipment create-shipment(event.data.order-id) send("order:shipped", {order-id: event.data.order-id, tracking: shipment.tracking}) } // Step 3: Notify on-order-shipped meta {on-event: "order:shipped", retry: 3} fn (event) { send-shipping-notification(event.data.order-id, event.data.tracking) } ``` Each step: - **Runs independently** — a failure in step 2 doesn't re-run step 1 - **Retries automatically** — with configurable attempts, backoff, and jitter - **Is fully observable** — inputs, outputs, and traces visible in Hot App - **Passes data forward** — via event payloads, not hidden internal state ## Long-Running Work For processes that outlive a single run — background jobs, data pipelines, real-time sessions — [Tasks](/docs/tasks) extend the same durability model with checkpoints and messaging. ## At-Least-Once Delivery Hot guarantees **at-least-once delivery** for events. Events are persisted before being acknowledged, and failed handlers retry automatically when configured. This means: - Events are never silently lost - Handlers will execute at least once for every event - Retries deliver the same event data - Infrastructure redelivery can deliver an event more than once - Strict event ordering is not guaranteed across concurrent workers If your handler has side effects that shouldn't happen twice (charging a payment, sending an email), use idempotency techniques — check whether the work was already done before doing it again. Queue envelopes are additive for rolling deploy compatibility. Workers hydrate the full event payload from the database before routing, so old queue messages remain readable while newer workers can still rely on the database as the source of truth. ```hot on-payment meta {on-event: "payment:charge", retry: 3} fn (event) { existing get-charge(event.data.idempotency-key) if(is-some(existing), existing, process-charge(event.data)) } ``` ## Observability All runs, events, retries, and tasks are visible in **Hot App**: - **Run history** with status, duration, and retry badges - **Execution traces** showing every function call and intermediate value - **Event flow** connecting runs to the events that triggered them - **Retry lineage** linking retries back to the original run via `origin_run_id` - **Real-time updates** as workflows execute You can filter by status, function, event type, or search across payloads to find exactly what you need. ## Summary | Concept | Role | |---------|------| | **Runs** | Atomic, persisted function executions with full traces | | **Events** | Persisted messages connecting runs into workflows | | **Retries** | Automatic re-execution with original input on failure | | **Tasks** | Long-running processes with checkpoints and messaging | | **Hot App** | Real-time visibility into every step | No replay engines, no hidden state machines. Each step is a function that takes input and produces output. The platform persists everything and handles recovery automatically. --- Source: https://hot.dev/docs/tasks # Tasks Tasks are long-running, asynchronous processes on the Hot Platform. They extend the platform run model with a durable resource, background execution, and task-specific lifecycle controls. See the [Platform Execution Model](/docs/platform/execution-model) for their place in event and stream lineage. ## Ordinary Runs vs Tasks | | Runs | Tasks | |---|------|-------| | **Duration** | Short-lived, synchronous | Long-running, asynchronous | | **Trigger** | HTTP requests, events, schedules | Started from runs or other tasks | | **Return** | Waits for completion, returns result | Returns immediately with `TaskInfo` | | **Execution record** | The run is the execution attempt | The task resource links to a task-type run | | **Use case** | Request-response, event handlers | Background jobs, containers, long-lived processes | ### Runs **Runs** are short-lived, synchronous top-level function execution attempts. Nested Hot function calls remain inside the current run's trace. Runs are triggered by: - HTTP requests (API calls, webhooks) - Events (`send`, `hot:call`) - Schedules (cron, dynamic schedules) Runs block until the function completes. See [Runs, Events & Streams](/docs/platform/runs-events-streams) for details. ### Tasks **Tasks** are long-running, asynchronous resources. When you start a task, the current run returns immediately with a `TaskInfo` containing the task ID and stream ID. The task inherits that stream, records the originating run, and executes in the background on a task worker. Its execution is recorded as a task-type run. There are two types of tasks: 1. **Code Tasks** — Hot code with messaging (`::hot::task/start`, `::hot::task/send`, `::hot::task/receive`) and WebSocket support (`::hot::ws`) 2. **Container Tasks** — Docker/OCI containers via `::hot::box/start` ### When to Use Each | Scenario | Use | |----------|-----| | Request-response, event handlers, scheduled jobs | **Runs** | | Long-running Hot code with send/receive messaging | **Code Tasks** | | Arbitrary languages, CLI tools, system binaries | **Container Tasks** | ## Task Lifecycle Tasks move through these states:
Task lifecycle states A queued task becomes running, then finishes as completed, failed, timed out, or cancelled. queued waiting for a worker claimed running task worker executing completed finished successfully failed exited with an error timed_out exceeded its timeout cancelled stopped cooperatively
| State | Description | |-------|-------------| | `queued` | Task is waiting for a worker | | `running` | Task is executing | | `completed` | Task finished successfully | | `failed` | Task exited with an error | | `timed_out` | Task exceeded its timeout | | `cancelled` | Task was cancelled before completion | ## Starting Tasks ### Code Tasks Use `::hot::task/start` to start a Hot function as a long-running task: ```hot ::task ::hot::task // Start a task with no arguments info ::task/start(::myapp/background-sync) // Start a task with arguments info ::task/start(::myapp/process-data, {url: "https://example.com"}) // Start with options (timeout, retry) info ::task/start(::myapp/long-job, {input: data}, { timeout: 3600000, retry: {attempts: 3, delay: 5000, backoff: "exponential"} }) ``` ### Container Tasks Use `::hot::box/start` to run Docker/OCI containers: ```hot ::box ::hot::box task ::box/start(BoxConf({ image: "python:3.13-alpine", cmd: ["python", "-c", "print('Hello')"], size: "nano" })) ``` See [Containers](/docs/box) for full container documentation. ## TaskInfo Both `::hot::task/start` and `::hot::box/start` return a `TaskInfo` with: | Field | Type | Description | |-------|------|-------------| | `id` | `Str` | Unique task identifier (UUID) | | `stream-id` | `Str` | Stream this task belongs to | For code tasks, `TaskInfo` also includes `stream` (the full stream object) and `origin-run` (the run that spawned the task). ## Waiting for Completion Choose where to wait based on who needs the result: - If later Hot code in the same execution depends on the result, call `::hot::task/await(info.id)`. - If a client needs the result, return `info.id` from the run and use the official SDK task waiter. This lets the originating run finish while the task continues asynchronously. All SDK waiters subscribe to `/v1/tasks/{task_id}/subscribe`. The first `task:update` is always the latest persisted state, so the client cannot miss a task that completed before it subscribed. The waiter reconnects when needed, returns the completed task record, and raises a structured task error for `failed`, `cancelled`, or `timed_out`. The task's existing stream also emits durable `task:update` snapshots. Subscribe to that stream when one client is coordinating several tasks; use the task-specific waiter when it only needs one task's terminal result. | Language | Wait method | |----------|-------------| | JavaScript / TypeScript | `await hot.tasks.wait(taskId)` | | Python | `hot.tasks.wait(task_id)` or `await async_hot.tasks.wait(task_id)` | | Go | `client.Tasks.Wait(ctx, taskID, nil)` | | Rust | `client.tasks().wait(task_id, TaskWaitOptions::default()).await` | | Java | `client.tasks().waitFor(taskId)` | See [SDKs](/docs/api/sdks#wait-for-a-background-task) for timeout examples and language-specific failure types. ## Cancellation Cancel a queued or running task with `::hot::task/cancel`: ```hot ::task ::hot::task info ::task/start(::myapp/long-job, data) // Later, cancel the task cancelled ::task/cancel(info.id) ``` Returns `true` if the task was cancelled, `false` if it was already in a terminal state. For running tasks, a cancellation message is delivered to the task's `receive` channel (as `{$cancel: true}`) so it can exit cooperatively. ## Messaging (Code Tasks Only) Code tasks can receive messages from other runs or tasks using `::hot::task/send` and `::hot::task/receive`: ```hot ::task ::hot::task // From a run: start a task and send it data info ::task/start(::myapp/worker, null) ::task/send(info.id, {command: "process", payload: data}) ::task/send(info.id, "shutdown") // Inside the task function: receive messages my-task fn (initial-args: Any): Any { msg ::task/receive() cond { eq(msg, "shutdown") => { "done" } => { process(msg) } } } ``` `receive` blocks until a message arrives. Returns `null` when the task's inbox closes. ## Checkpoint & Restore (Code Tasks) Long-running code tasks can save application state that persists across restarts. If a task is interrupted (worker crash, deploy) and retried, the new instance can call `restore()` to pick up where it left off. ```hot ::task ::hot::task my-etl fn (config: Map): Any { // Restore previous state, or start fresh state or(::task/restore(), {offset: 0, processed: 0}) // ... process batch starting from state.offset ... // Save progress ::task/checkpoint({offset: add(state.offset, batch-size), processed: add(state.processed, batch-size)}) } ``` `checkpoint` accepts any serializable value and returns `true` on success. `restore` returns the last checkpointed value, or `null` if no checkpoint exists. Both are only callable from inside a task. You can also inspect a different task's checkpoint by passing a task ID: `::task/restore(task-id)`. ## WebSocket Support (Code Tasks) Code tasks can maintain long-lived WebSocket connections using `::hot::ws`: ```hot ::ws ::hot::ws // Inside a task conn ::ws/connect("wss://echo.websocket.org", {headers: {}}) ::ws/send(conn, {type: "hello", text: "world"}) msg ::ws/receive(conn) ::ws/close(conn) ``` WebSocket connections outlive a single run, making them ideal for real-time sessions inside tasks. --- Source: https://hot.dev/docs/events # 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. One event can select zero, one, or multiple handlers. Each selected handler invocation becomes a run in the event's stream. See the [Platform Execution Model](/docs/platform/execution-model) for the complete event, run, task, and stream lifecycle. ## Event Handlers Define event handlers using the `on-event` metadata: ```hot ::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 handlers can be grouped under an [agent](/docs/agents) by adding `agent: TypeName` to the metadata. This enables per-agent run tracking, health metrics, and observability in the Hot App. ## Event Schemas Hot events appear in two related shapes: ### 1) Hot language shape (`send`) When publishing from Hot code, the event shape is: | Field | Type | Description | |-------|------|-------------| | `type` | `Str` | Event name (for example `"user:created"`) | | `data` | `Any` | Event payload | `send("user:created", {...})` uses this shape. ### 2) HTTP API shape (`POST /v1/events`) When publishing through the Hot API, the request body is: | Field | Type | Required | Description | |-------|------|----------|-------------| | `event_type` | `string` | Yes | Event name | | `event_data` | `json` | Yes | Event payload | | `stream_id` | `uuid` | No | Append to an existing stream instead of creating a new one | Response payload fields include: | Field | Description | |-------|-------------| | `event_id` | Published event UUID | | `stream_id` | Stream UUID containing this event | | `event_type` | Event name | | `event_data` | Event payload | | `event_time` | Event timestamp | ## System Events in `hot-std` `hot-std` defines built-in handlers for several reserved `hot:*` event types. These power core platform behavior. | Event Type | Purpose | Expected Payload (`event.data`) | |------------|---------|----------------------------------| | `hot:call` | Execute a function asynchronously | `{fn: "::ns/var", args: [...]}` | | `hot:schedule` | Internal scheduler trigger for scheduled functions | `{fn: "::ns/var", args: [...]}` | | `hot:schedule:new` | Create a dynamic one-time or recurring schedule | `{fn: "::ns/var", args: [...], schedule: "..."}` | | `hot:schedule:cancel` | Cancel a dynamic schedule | `{schedule-id: "uuid"}` or `{fn: "::ns/var"}` | These handlers are defined in `hot/pkg/hot-std/src/hot/lang.hot`. > The `hot:` namespace is reserved for system behavior. Prefer your own event namespace (for example `user:created`, `billing:invoice-paid`) for application events. ## Sending Events Send events from your code to trigger handlers: ```hot // Send an event (send is a core function) send("user:created", { id: user-id, email: email, name: name }) ``` Or send events via the Hot API: ```bash curl -X POST https://api.hot.dev/v1/events \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_type": "user:created", "event_data": {"id": "123", "email": "user@example.com"}}' ``` ## Background Jobs Any function can be executed as a background job by sending a `hot:call` event: ```bash # 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 '{ "event_type": "hot:call", "event_data": { "fn": "::myapp::jobs/process-order", "args": [{"order_id": "12345"}] } }' ``` Or from Hot code: ```hot // 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: ```bash curl https://api.hot.dev/v1/events/$EVENT_ID/runs \ -H "Authorization: Bearer $HOT_API_KEY" ``` ## Retries Event handlers can retry automatically when they fail: ```hot on-payment-received meta { on-event: "payment:received", retry: 3 } fn (event) { update-account-balance(event.data) } ``` For full retry configuration (attempts, delay, backoff, jitter, and limits), see [Retries](/docs/retries). --- Source: https://hot.dev/docs/agents # Agents Agents are typed groups of event handlers, schedules, and webhooks that share identity. An agent is defined as a Hot type with `agent` metadata, and functions declare membership via `meta {agent: TypeName}`. When deployed, Hot tracks agent runs, surfaces health metrics, and groups observability data by agent. Agents group handlers by type reference, giving you compile-time validation and structured config fields. For a complete runnable example, see the [Hot Chat demo](/docs/demos/hot-chat) — one Hot project that boots two AI agents (session-first Team Mode and identity-first Personal Mode) behind a polished Next.js client, over the same typed-event wire contract a Slack or Telegram adapter would use. ## Defining an Agent An agent starts with a type definition that has `agent` in its metadata. The type's struct fields become the agent's configuration, and the `doc` or `agent.description` provides a human-readable summary. ### Basic Example ```hot ::myapp::support ns SupportAgent meta { doc: """AI-powered customer support agent""", agent: { name: "Support Agent", tags: ["support", "ai"], }, } type { model: Str, system: Str, escalation-channel: Str, } ``` This registers `SupportAgent` as an agent. The type name is the identifier; `name` is a display label for the [Hot App](/docs/app#agents). ### Full Example ```hot ::acme::support ns ::store ::hot::store ::ctx ::hot::ctx EmbeddingOptions ::store/EmbeddingOptions SupportAgent meta { doc: """ Customer support agent that responds to tickets, searches a knowledge base, escalates when uncertain, and reviews interactions daily. """, agent: { name: "Support Agent", description: "AI-powered support with semantic KB search and escalation", tags: ["support", "ai", "customer-facing"], }, } type { model: Str, system: Str, escalation-channel: Str, tone: Str, } support-agent SupportAgent({ model: ::ctx/get("support.model", "claude-sonnet"), system: ::ctx/get("support.system", "You are a helpful support agent."), escalation-channel: ::ctx/get("support.escalation", "#support"), tone: ::ctx/get("support.tone", "professional"), }) // Shared knowledge base (static name, safe at namespace level) kb ::store/Map({name: "support:kb", embedding: EmbeddingOptions.Default}) on-ticket meta {agent: SupportAgent, on-event: "support:ticket"} fn (event) { // Per-stream memory (needs event.stream-id, so created inside the handler) memory ::store/Map({name: `support:${event.stream-id}`, embedding: EmbeddingOptions.Default}) context ::store/search(kb, event.data.message, {limit: 5}) history ::store/search(memory, event.data.message, {limit: 10}) response generate-reply(support-agent, context, history, event.data.message) ::store/put(memory, Uuid(), {role: "assistant", content: response}) send("support:response", {ticket-id: event.data.ticket-id, response: response}) } on-feedback meta {agent: SupportAgent, on-event: "support:feedback"} fn (event) { memory ::store/Map({name: `support:${event.stream-id}`, embedding: EmbeddingOptions.Default}) ::store/put(memory, Uuid(), {type: "feedback", rating: event.data.rating}) } daily-review meta {agent: SupportAgent, schedule: "0 9 * * 1-5"} fn (event) { summarize-yesterday(kb) } on-escalation meta {agent: SupportAgent, on-event: "support:escalate"} fn (event) { ::hot::slack/post-message(support-agent.escalation-channel, `Needs help: ${event.data.reason}`) } ``` This defines one agent with four handlers: two event-driven, one scheduled, and one for escalation. All share the `support-agent` instance for configuration and `::hot::store` maps for memory. ## Agent Metadata The `agent` key in the type's metadata is a map with the following fields: | Field | Required | Description | |-------|----------|-------------| | `name` | No | Display name for the agent. Falls back to the type name (e.g., `SupportAgent`). | | `description` | No | Short description for the agent. Falls back to the top-level `doc` metadata. | | `tags` | No | List of strings for categorization and filtering in the Hot App. | The top-level `doc` metadata serves as the default description. If both `doc` and `agent.description` are present, `agent.description` takes priority in agent-specific contexts (the App dashboard, API responses). ## Grouping Handlers Functions declare membership in an agent via `meta {agent: TypeName}`. This works with all handler types: ### Event Handlers ```hot on-ticket meta {agent: SupportAgent, on-event: "support:ticket"} fn (event) { process-ticket(event.data) } ``` ### Scheduled Functions ```hot daily-review meta {agent: SupportAgent, schedule: "0 9 * * 1-5"} fn (event) { review-interactions() } ``` ### Webhooks ```hot on-stripe-payment meta { agent: BillingAgent, webhook: {service: "billing", path: "/stripe"}, } fn (request) { process-payment(or(request.body, {})) {ok: true} } ``` The `agent` reference is a type name, not a string. The compiler resolves it to the agent type, catching typos at compile time. A single agent can have any number of handlers across event handlers, schedules, and webhooks. ## Agents vs Workflows Agents and workflows are related but separate: - **Agents** describe actor/runtime identity. They own config fields, runtime attribution, health metrics, runs, streams, and memory patterns. - **Workflows** describe process topology. They group handlers, events, schedules, webhooks, and MCP tools into a named flow, but a workflow definition is not required for Hot to discover the flow. Named workflows use typed definitions, similar to agents: ```hot LeadQualification meta { doc: "Scores inbound leads and routes sales or nurture outcomes", workflow: { name: "Lead Qualification", tags: ["sales", "ai"], }, } type {} ``` Handlers can opt into one or more named workflows (here `LeadQualifier` is the agent type defined in [Hybrid Agent](#hybrid-agent) below): ```hot qualify-lead meta { agent: LeadQualifier, workflows: [LeadQualification], on-event: "lead:new", } fn (event) { score enrich-and-score(event.data) send("lead:qualified", merge(event.data, {score: score})) } ``` If a handler has no `agent` or `workflow` metadata, Hot still records its triggers and sends. The Hot App can show these as unnamed project-level workflows in the environment-wide workflow graph. This keeps observability complete while letting you add names only where they are useful. ## Event Sends When a handler calls `send("event-name", data)`, the compiler detects this statically and records the event name in the handler's metadata. This powers the [Agent Graph](#agent-graph) — sends appear as outgoing edges from the handler to the target event type — and the [code documentation generator](/docs/app#docs), which shows sends on every documented function. ### Automatic Detection Static send extraction works out of the box. The compiler scans function bodies for `send()` calls and resolves the event name from literal strings or namespace-level constants: ```hot on-order meta {agent: OrderAgent, on-event: "order:created"} fn (event) { validate(event.data) send("inventory:reserve", event.data) send("audit:log", {action: "order-created", order-id: event.data.id}) } ``` After compilation, this handler's metadata will include `sends: ["inventory:reserve", "audit:log"]` automatically. No annotation is needed. Variable references are resolved when the value is a string constant in the same namespace: ```hot inventory-event "inventory:reserve" on-order meta {agent: OrderAgent, on-event: "order:created"} fn (event) { send(inventory-event, event.data) } ``` Dynamic event names (e.g., `send(event.type, data)`) cannot be resolved statically and are silently skipped. ### Manual `sends` Declarations You can declare sends explicitly in `meta` to document events that are dynamically generated, or to add descriptions: ```hot on-order meta { agent: OrderAgent, on-event: "order:created", sends: ["inventory:reserve", "audit:log"], } fn (event) { send("inventory:reserve", event.data) send("audit:log", {action: "order-created"}) } ``` Entries can be strings or rich objects with a `doc` field for documentation: ```hot on-order meta { agent: OrderAgent, on-event: "order:created", sends: [ {event: "inventory:reserve", doc: "Reserve stock for the ordered items"}, {event: "audit:log", doc: "Record order creation for compliance"}, ], } fn (event) { send("inventory:reserve", event.data) send("audit:log", {action: "order-created"}) } ``` Rich object descriptions appear in the inspector panel beneath each send edge in the [Agent Graph](#agent-graph). ### Merge Behavior Manual and static sends are merged together: - Existing manual `sends` entries are preserved as-is (including rich objects with `doc`) - Statically detected event names are added alongside, deduplicated - For the same event name, the manual declaration takes precedence This means you can rely on automatic detection for most cases and add manual declarations only when you need richer metadata or have dynamically generated event names. ### Non-Agent Functions Send extraction works for **all** functions, not just agent-tagged handlers. If a standalone event handler or scheduled function calls `send()`, the event names are recorded in its metadata. The [code documentation generator](/docs/app#docs) surfaces sends on every documented function as a badge and detail listing. ## Config Fields The agent type's struct fields define its configuration. These are the per-deployment knobs — model selection, system prompts, channel names, thresholds. ```hot SupportAgent meta { agent: {name: "Support Agent"}, } type { model: Str, system: Str, escalation-channel: Str, confidence-threshold: Dec, } ``` Create an instance using [context variables](/docs/app#context-variables) for environment-specific values: ```hot support-agent SupportAgent({ model: ::ctx/get("support.model", "claude-sonnet"), system: ::ctx/get("support.system", "You are a helpful support agent."), escalation-channel: ::ctx/get("support.escalation", "#support"), confidence-threshold: Dec(::ctx/get("support.threshold", "0.85")), }) ``` Handlers reference the instance directly — `support-agent.model`, `support-agent.escalation-channel`. Config fields are visible in the Agent Dashboard's Overview tab. ## Agent Runs When a handler with `meta {agent: TypeName}` executes, the run is automatically tagged with the agent's qualified name (e.g., `::acme::support/SupportAgent`). This tagging enables: - **Filtering** — view runs by agent in the Hot App - **Metrics** — per-agent success rate, average duration, and run count - **Health monitoring** — the Dashboard shows agent health with color-coded indicators - **Attribution** — trace any run back to the agent that produced it No additional code is needed. The agent tagging happens at the runtime level when a handler declares `meta {agent: ...}`. ## Agent Skills Agents that use `hot.dev/hot-ai` can expose prompt skills to the model. A skill is a named instruction bundle that the chat run loop advertises through `list_skills`, `read_skill`, and `apply_skill`. Prompt-only skills are `::ai::skill/Skill` values: ```hot ::skill ::ai::skill support-tone ::skill/Skill({ name: "support-tone", description: "How to answer customer support questions", when: ["support reply", "refund", "angry customer"], body: """ Be concise, empathetic, and concrete. Cite the policy or next step. """, }) ``` Function-backed skills are also supported with `meta {skill: ...}` and `::skill/from-fn`, but those functions are metadata sources. The built-in skill tools read `body` or call `body-fn`; they do not invoke the skill function itself. Markdown-authored skills live in project resources as `*.skill.md` files. Skill codegen turns them into generated `.skill.hot` files that export `Skill` values, so they can be listed directly in `::skill/for-agent([...])`. Because generated skills construct `::ai::skill/Skill`, projects with Markdown skill resources must declare a `hot.dev/hot-ai` dependency: ```hot hot.project.support.deps { "hot.dev/hot-ai": {} } ``` ## Agent Memory Agents use [`::hot::store`](/pkg/hot.dev/hot-std/hot/store) for persistent memory. Store maps support optional embedding-based semantic search, which is useful for knowledge bases and conversation history. Store data is scoped to the current Hot organization and environment and is stored in the main Hot database. This means agent memory works in project, worker, and deployed runtime contexts; it is not a standalone filesystem-backed mode like `::hot::file` direct access. ### Per-Stream Memory Each stream can have its own memory, isolated by stream ID. Since the map name depends on the stream ID, create it inside the handler where `event` is available: ```hot EmbeddingOptions ::hot::store/EmbeddingOptions on-ticket meta {agent: SupportAgent, on-event: "support:ticket"} fn (event) { memory ::store/Map({name: `support:${event.stream-id}`, embedding: EmbeddingOptions.Default}) history ::store/search(memory, event.data.message, {limit: 10}) ::store/put(memory, Uuid(), {role: "user", content: event.data.message}) response generate-reply(support-agent, history, event.data.message) ::store/put(memory, Uuid(), {role: "assistant", content: response}) } ``` ### Shared Knowledge Base A knowledge base shared across all streams uses a static name, so the map definition goes at the namespace level. Handlers search it; a separate handler or schedule populates it: ```hot EmbeddingOptions ::hot::store/EmbeddingOptions kb ::store/Map({name: "support:kb", embedding: EmbeddingOptions.Default}) on-ticket meta {agent: SupportAgent, on-event: "support:ticket"} fn (event) { context ::store/search(kb, event.data.message, {limit: 5}) } seed-kb meta {agent: SupportAgent, on-event: "kb:seed"} fn (event) { ::store/put-many(kb, { "returns": {title: "Returns", content: "Refunds available within 30 days of purchase."}, "shipping": {title: "Shipping", content: "Free shipping on orders over $50."}, }) } ``` ### Plain Key-Value State Not all agent state needs embeddings. Use plain maps for counters, flags, and structured data: ```hot counters ::store/Map({name: "support:counters"}) on-ticket meta {agent: SupportAgent, on-event: "support:ticket"} fn (event) { total or(::store/get(counters, "total-tickets"), 0) ::store/put(counters, "total-tickets", add(total, 1)) } ``` ## Lifecycle Agents are metadata-driven — they're discovered from your source code and registered automatically: 1. **Define** — Add `agent` metadata to a type and `meta {agent: TypeName}` to handler functions 2. **Deploy** — Run `hot deploy` (or `hot dev` for local development). The compiler scans types for `agent` metadata and registers agent definitions. 3. **Execute** — When events arrive, schedules fire, or webhooks receive requests, handlers run with agent attribution. Each run is tagged with the agent's qualified name. 4. **Observe** — View agent health, metrics, handlers, and runs in the [Hot App](/docs/app#agents) When you redeploy, agent definitions are updated automatically. If a type's `agent` metadata is removed, the agent is unregistered. If handler functions remove their `agent` reference, those handlers still execute but are no longer attributed to the agent. ## Handler Documentation Add a `doc` field to any handler's metadata to provide a description. This description appears in the Agent Graph inspector panel when you click a handler node: ```hot nurture-lead meta { doc: "Nurtures leads by sending a welcome email and re-queueing for scoring", agent: LeadQualifier, on-event: "lead:nurture", sends: [ {event: "email:send", doc: "Send welcome drip email"}, {event: "lead:new", doc: "Re-queue for scoring after nurture delay"}, ], } fn (event) { send-drip-email(event.data.email) send("lead:new", event.data) } ``` The `doc` field works on all handler types: event handlers, scheduled functions, webhooks, and MCP tools. For MCP tools, the MCP `description` field takes priority; `doc` is used as a fallback when no MCP description is provided. ## Viewing Agents in the App The Hot App provides dedicated views for agents. See [Hot App > Agents](/docs/app#agents) for details. ### Agents List The **Agents** page shows all deployed agents as a card grid. Each card displays the agent name, namespace, description, tags, handler count, and project. Use the search bar to filter by name, namespace, or project. A topology graph spanning all agents is available at the top of the page. ### Agent Dashboard Click an agent to open its dashboard. The default view is the **Graph** tab, which shows the agent's topology: - **Graph tab** — Interactive topology graph showing the agent's handlers, triggers (events, schedules, webhooks, MCP tools), and event sends as a directed graph. Click any node to open the inspector panel with details. Toggle between horizontal and vertical layouts using the direction buttons in the toolbar. - **Handlers tab** — All event handlers, schedules, and webhooks linked to this agent with trigger details, retry config, and source locations - **Runs tab** — Paginated run history filtered to this agent - **Streams tab** — Streams where this agent participated ### Agent Graph The agent graph visualizes the flow of data through an agent. Nodes represent two categories: - **Functions** (blue icons) — Event handlers, scheduled functions, webhook handlers, and MCP tool handlers. Each shows the function name and agent membership. - **Triggers** (green icons) — Events, schedules, webhooks, and MCP tools that invoke functions. Each uses a distinct icon to indicate its type. Edges show the relationships: triggers connect to the handlers they invoke, and handlers connect to the events they send. **Inspector panel** — Click any node to open the inspector sidebar, which shows the node's full details: namespace, source file location, retry configuration, description (from `doc` metadata), handled events, and sent events. For send edges with `doc` annotations, the description appears below the event name. Toggle the inspector with the panel icon in the toolbar. **Layout** — Switch between horizontal (left-to-right) and vertical (top-to-bottom) layouts using the direction toggle in the toolbar. **Download** — Export the graph as a PNG image using the download button. **Zoom** — Use the zoom slider or mouse wheel to zoom in and out on large graphs. ### Dashboard Health Widget The main Dashboard includes an **Agent Health** widget showing each deployed agent with a health indicator: - **Green dot** — 95%+ success rate - **Yellow dot** — 80–95% success rate - **Red dot** — Below 80% success rate The widget also shows agent vs. non-agent run counts, giving you a quick sense of how much of your workload is agent-driven. ## Patterns ### Event-Driven Agent The most common pattern. The agent responds to external events: ```hot InboxTriager meta {agent: {name: "Inbox Triager", tags: ["email"]}} type { rules: Vec } on-email meta {agent: InboxTriager, on-event: "email:received"} fn (event) { classify-and-route(event.data) } ``` ### Scheduled Agent An agent that runs on a schedule: ```hot DailyBriefing meta {agent: {name: "Daily Briefing", tags: ["reporting"]}} type { sources: Vec, channel: Str } briefing DailyBriefing({ sources: ["github", "stripe", "analytics"], channel: ::ctx/get("briefing.channel", "#general"), }) morning-report meta {agent: DailyBriefing, schedule: "0 8 * * 1-5"} fn (event) { data aggregate-sources(briefing.sources) summary generate-summary(data) send-to-channel(briefing.channel, summary) } ``` ### Hybrid Agent Combines events, schedules, and webhooks: ```hot LeadQualifier meta { agent: {name: "Lead Qualifier", tags: ["sales", "ai"]}, } type { model: Str, threshold: Dec, crm-key: Str } qualifier LeadQualifier({ model: ::ctx/get("leads.model", "claude-sonnet"), threshold: Dec(::ctx/get("leads.threshold", "0.7")), crm-key: ::ctx/get("leads.crm-key"), }) on-signup meta {agent: LeadQualifier, webhook: {service: "leads", path: "/signup"}} fn (request) { send("lead:new", or(request.body, {})) {ok: true} } qualify-lead meta {agent: LeadQualifier, on-event: "lead:new"} fn (event) { score enrich-and-score(event.data, qualifier.model) if(gte(score, qualifier.threshold), send("lead:qualified", merge(event.data, {score: score})), send("lead:nurture", merge(event.data, {score: score}))) } weekly-pipeline meta {agent: LeadQualifier, schedule: "0 9 * * 1"} fn (event) { generate-pipeline-report() } ``` ## Best Practices **Name agents by domain responsibility.** An agent should own a coherent area of functionality. `SupportAgent`, `BillingAgent`, and `LeadQualifier` are clear; `UtilityAgent` or `MainAgent` are not. **Keep handler count focused.** Each agent should have a small number of handlers with a clear purpose. If an agent has more than 8–10 handlers, consider splitting it into separate agents. **Use config fields for tunable parameters.** Model names, thresholds, channel names, and system prompts belong in config fields. This makes agents reusable across environments without code changes. **Use tags for categorization.** Tags like `["support", "ai"]` or `["billing", "webhook"]` help organize agents in the App, especially as the number of deployed agents grows. **Use `::hot::store` for agent memory.** Enable embeddings when you need semantic search (conversation history, knowledge bases). Use plain maps for counters, state flags, and structured data. **Use streams for multi-step workflows.** Runs triggered from inside a handler automatically share that handler's stream, so chained `send(...)` calls stay grouped. When publishing from outside Hot, pass a `stream_id` to `POST /v1/events` to append to an existing stream. Either way, the Streams view gives you end-to-end visibility into agent workflows. **Prefer event-driven over polling.** Use `on-event` handlers to react to changes rather than scheduled polling. Events are more efficient and produce clearer audit trails. --- Source: https://hot.dev/docs/schedules # Schedules The Hot Scheduler runs functions on a schedule using cron expressions or natural language. ## Scheduled Runs Schedule functions to run at specific times using the `schedule` metadata: ```hot ::myapp::jobs ns // Run every hour hourly-cleanup meta {schedule: "0 * * * *"} fn (event) { cleanup-expired-sessions() prune-old-logs() } // Run daily at midnight UTC daily-report meta {schedule: "0 0 * * *"} fn (event) { generate-daily-report() send-to-slack() } // Run every 5 minutes health-check meta {schedule: "*/5 * * * *"} fn (event) { check-external-services() } ``` Scheduled functions can be grouped under an [agent](/docs/agents) by adding `agent: TypeName` to the metadata. This enables per-agent run tracking, health metrics, and observability in the Hot App. ## Cron Expression Format ``` ┌───────────── second (0-59, optional) │ ┌───────────── minute (0-59) │ │ ┌───────────── hour (0-23) │ │ │ ┌───────────── day of month (1-31) │ │ │ │ ┌───────────── month (1-12 or JAN-DEC) │ │ │ │ │ ┌───────────── day of week (0-6 or SUN-SAT) │ │ │ │ │ │ ┌───────────── year (optional) │ │ │ │ │ │ │ * * * * * * * ``` Hot accepts common cron forms (5, 6, or 7 fields) plus nickname forms such as `@daily`. Common patterns: | Pattern | Description | |---------|-------------| | `* * * * *` | Every minute | | `*/15 * * * * *` | Every 15 seconds | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour | | `0 0 * * *` | Daily at midnight | | `0 0 * * 0` | Weekly on Sunday | | `0 0 1 * *` | Monthly on the 1st | | `@daily` | Daily (nickname form) | ## Natural Language Schedules You can also use plain English to define schedules: ```hot ::myapp::jobs ns // Natural language schedules daily-digest meta {schedule: "every day at 9:00 am"} fn (event) { send-daily-digest() } weekly-review meta {schedule: "on Sunday at 12:00"} fn (event) { generate-weekly-review() } payroll meta {schedule: "run at midnight on the 1st and 15th of the month"} fn (event) { process-payroll() } ``` Supported English patterns: | English Phrase | Equivalent Cron | |----------------|-----------------| | `every minute` | `* * * * *` | | `every 15 seconds` | `*/15 * * * * *` | | `every day at 4:00 pm` | `0 0 16 */1 * ? *` (equivalent) | | `at 10:00 am` | `0 0 10 * * ? *` (equivalent) | | `run at midnight on the 1st and 15th of the month` | `0 0 0 1,15 * ? *` (equivalent) | | `on Sunday at 12:00` | `0 0 12 ? * SUN *` (equivalent) | | `7pm every Thursday` | `0 0 19 ? * THU *` (equivalent) | | `midnight on Tuesdays` | `0 0 0 ? * TUE *` (equivalent) | Natural language schedules are converted to cron expressions internally. Both formats are fully supported and can be mixed within the same project. ## Dynamic Schedules In addition to metadata-driven schedules (defined at build time), you can create schedules dynamically at runtime using events. This is useful for: - **One-time scheduled calls** - Execute a function once at a specific time - **User-triggered scheduling** - Let users schedule tasks for later - **Dynamic recurring jobs** - Create cron schedules based on runtime conditions ### Creating Schedules Use the `hot:schedule:new` event to create a schedule: ```hot // Schedule for a specific datetime send("hot:schedule:new", { fn: "::myapp::orders/process-order", args: [{order_id: "12345"}], schedule: "2024-01-15T10:30:00Z" }) // Schedule for 10 minutes from now send("hot:schedule:new", { fn: "::myapp::reminders/send-reminder", args: [{user_id: user.id, message: "Time to check in!"}], schedule: "in 10 minutes" }) // Schedule with natural language duration send("hot:schedule:new", { fn: "::myapp::notifications/send-followup", args: [{email: customer.email}], schedule: "2 hours from now" }) // Create a recurring schedule dynamically send("hot:schedule:new", { fn: "::myapp::reports/generate", args: [{report_type: "daily"}], schedule: "every day at 9am" }) ``` The `hot:schedule:new` event payload uses: | Field | Type | Required | Description | |-------|------|----------|-------------| | `fn` | `Str` or function reference | Yes | Target function (for example `::myapp::jobs/process`) | | `args` | `Vec` | No | Function arguments. Defaults to `[]` when omitted/null. | | `schedule` | `Str` or `DateTime` | Yes | When to run (one-time or recurring expression) | ### Schedule Formats The `schedule` field supports multiple formats: | Format | Example | Description | |--------|---------|-------------| | ISO 8601 datetime | `"2024-01-15T10:30:00Z"` | Execute at exact time | | Duration | `"10 minutes"`, `"2h"`, `"1 day 3 hours"` | Execute after duration | | Natural language | `"in 10 minutes"`, `"2 hours from now"` | Human-friendly durations | | Cron expression | `"0 30 9 * * MON"` | Recurring schedule | | English cron | `"every day at 9am"`, `"every Monday at 2 PM"` | Natural language recurring | ### Cancelling Schedules Cancel pending schedules using the `hot:schedule:cancel` event: ```hot // Cancel by schedule ID (returned from hot:schedule:new) send("hot:schedule:cancel", { schedule-id: "01916d8a-9c12-7f00-8000-123456789abc" }) // Cancel all schedules for a specific function send("hot:schedule:cancel", { fn: "::myapp::jobs/heavy-process" }) ``` The `hot:schedule:cancel` event payload supports either: | Field | Type | Required | Description | |-------|------|----------|-------------| | `schedule-id` | `Str` (UUID) | Conditionally | Cancel one schedule by ID | | `fn` | `Str` or function reference | Conditionally | Cancel active schedules for a function | At least one of `schedule-id` or `fn` must be provided. Cancelling schedules is useful for: - Removing pending one-time schedules that are no longer needed - Disabling recurring schedules without redeploying - Cleaning up after a user cancels an action ### Dynamic vs Metadata Schedules | Feature | Metadata Schedule | Dynamic Schedule | |---------|-------------------|------------------| | Defined in | Hot code (`meta {schedule: ...}`) | Runtime via `hot:schedule:new` | | Lifecycle | Tied to build/deployment | Can be created/cancelled anytime | | One-time support | No | Yes | | Visible in UI | Always | When active | | Use case | Regular jobs (daily reports, cleanup) | User-triggered, conditional scheduling | ### Example: Scheduled Reminders ```hot ::myapp::reminders ns // Schedule a reminder for later schedule-reminder fn (user-id: Str, message: Str, delay: Str): Str { // Create a one-time schedule schedule-id send("hot:schedule:new", { fn: "::myapp::reminders/send-reminder", args: [{user-id: user-id, message: message}], schedule: delay }) // Return the schedule ID so it can be cancelled if needed schedule-id } // Cancel a pending reminder cancel-reminder fn (schedule-id: Str): Bool { send("hot:schedule:cancel", {schedule-id: schedule-id}) } // The actual reminder function (called by scheduler) send-reminder fn (data: Map) { user get-user(data.user-id) send-push-notification(user.device-token, data.message) } ``` Usage: ```hot // Schedule a reminder for 30 minutes from now reminder-id schedule-reminder("user-123", "Time for your meeting!", "in 30 minutes") // Later, cancel if user dismisses early cancel-reminder(reminder-id) ``` ## Retries Scheduled functions can retry automatically when they fail: ```hot import-data meta { schedule: "0 2 * * *", retry: 5 } fn (event) { import-from-sftp() } ``` For full retry configuration (attempts, delay, backoff, jitter, limits, and `pending_retry` behavior), see [Retries](/docs/retries). ## How It Works When a scheduled function's time arrives, the scheduler sends a `hot:schedule` event with the function details. A worker picks up the event and executes the function. The function is called with a single argument describing the schedule that fired: ```hot nightly-sync meta {schedule: "0 2 * * *"} fn (event) { // event.type → "hot:schedule" // event.schedule_id → UUID of the schedule that fired // event.scheduled_at → RFC 3339 timestamp of the trigger sync-data() } ``` Declare the parameter even if you don't use it — the scheduler always passes it. Scheduled runs are tracked and visible in the Hot App alongside event-triggered and API-triggered runs. --- Source: https://hot.dev/docs/retries # Retries Hot supports automatic retries for: - **Event handlers** (`meta {on-event: "...", retry: ...}`) - **Scheduled functions** (`meta {schedule: "...", retry: ...}`) Retries are configured with the `retry` metadata field. > `retry` does **not** apply to synchronous request/response surfaces like MCP tool calls and webhook invocations. ## Retry Metadata The `retry` field supports two forms. Simple form: ```hot retry: 3 ``` Full form: ```hot retry: { attempts: 5, delay: 5000, backoff: "exponential", max_delay: 120000, jitter: true } ``` | Field | Description | Default | |-------|-------------|---------| | `attempts` (or simple number) | Maximum retry attempts | `0` (disabled) | | `delay` | Base delay between retries (ms) | `1000` | | `backoff` | Delay strategy: `"fixed"`, `"exponential"`, `"linear"` | `"fixed"` | | `max_delay` | Delay cap for backoff strategies (ms) | `300000` | | `jitter` | Add random jitter (about +/-10%) | `false` | Backoff formulas: - `"fixed"`: `delay` - `"exponential"`: `delay * 2^attempt` - `"linear"`: `delay * (attempt + 1)` ## How Retries Execute When a retryable run fails: 1. Hot reads retry config from function metadata. 2. If attempts remain, the run is updated to `pending_retry`. 3. A new run is scheduled at `next_retry_at`. 4. The retry run is linked to the original via `origin_run_id`. 5. Retries stop on success or when attempts are exhausted. In Hot App, retries are shown with retry badges (for example `↻1`, `↻2`) and linked run history. ## Limits and Clamping Retry values are clamped to configured platform limits: | Setting | Environment Variable | Default | |---------|----------------------|---------| | Max attempts | `HOT_RETRY_MAX_ATTEMPTS` | `10` | | Max delay | `HOT_RETRY_MAX_DELAY_MS` | `3600000` (1 hour) | | Default delay | `HOT_RETRY_DEFAULT_DELAY_MS` | `1000` | Values below minimum delay are raised, and values above configured limits are capped. --- Source: https://hot.dev/docs/alerts # Alerts The Alerts system helps you monitor your Hot applications by automatically notifying you when important events occur, such as run failures or deployment issues. Alerts use a pub/sub model with three key components: **channels**, **destinations**, and **subscriptions**. ## How Alerts Work 1. An **alert event** fires (e.g., a run fails, or your code calls `alert()`) 2. The event name is matched against **channel** patterns 3. Matching channels trigger their **subscriptions** 4. Each subscription delivers the alert to one or more **destinations** (email, Slack, PagerDuty, webhook) ## Alert Channels **Alert Channels** define the types of events that can trigger alerts. Channels use regex patterns to match alert event names. ### Built-in System Channels Hot provides several built-in channels that are automatically available: | Channel | Description | |---------|-------------| | `run:failed` | Triggered when a run fails (after all retries are exhausted). Payload includes `run_id`, `env_id`, `error`, and `timestamp`. | | `run:cancelled` | Triggered when a run is cancelled. Payload includes `run_id`, `env_id`, and `timestamp`. | | `deploy:failed` | Triggered when a deployment fails during worker processing (e.g., build extraction, storage retrieval, or handler loading). Payload includes `build_id`, `env_id`, `error`, and `timestamp`. | | `deploy:succeeded` | Triggered when a deployment is fully processed and the build is ready to serve traffic. Payload includes `build_id`, `env_id`, and `timestamp`. | These system channels are read-only and available to all organizations. ### Custom Channels You can create custom channels with regex patterns to match specific alert types. For example: - `run:.*` - Matches all run-related alerts - `deploy:.*` - Matches all deployment-related alerts - `payment:.*` - Matches custom payment-related alerts (triggered from Hot code via `alert()`) Custom channels are scoped to your organization and can optionally be environment-specific. They can be created from the Channels tab by organization admins. ## Alert Destinations **Alert Destinations** are the endpoints where alerts are delivered. Destinations are configured at the organization level and can be reused across multiple subscriptions and environments. Hot supports four destination types: | Type | Description | Configuration Fields | |------|-------------|---------------------| | **Email** | Send alerts to email addresses | Email address | | **Slack** | Post alerts to Slack channels | Webhook URL, optional channel override | | **PagerDuty** | Create PagerDuty incidents | Routing key (Integration Key), severity level | | **Webhook** | POST alerts to custom HTTP endpoints | URL, optional custom headers (JSON) | Each destination has a name, type, and can be enabled or disabled independently. All destination management requires organization admin permissions. ## Alert Subscriptions **Alert Subscriptions** connect channels to destinations. When an alert event matches a channel pattern, all active subscriptions for that channel will deliver the alert to their configured destinations. Subscriptions can be configured at two scopes: - **Organization-wide** - Alerts are sent for all environments in the organization - **Environment-specific** - Alerts are sent only for a specific environment Each subscription can include multiple channels and multiple destinations, allowing you to route different types of alerts to different notification endpoints. **Example workflow:** 1. Create an email destination: `ops-team@example.com` 2. Create a Slack destination: `#alerts` channel 3. Create a subscription that routes `run:failed` alerts to both destinations 4. When a run fails, both the email and Slack destinations receive the alert Subscriptions can be enabled or disabled without deleting them, making it easy to temporarily pause notifications. ## Alert History The **History** tab displays all triggered alerts with their delivery status. Each alert entry shows: - The alert channel that was triggered - When the alert was created - A delivery summary (sent, pending, failed counts) Click on an individual alert to see the full payload and detailed delivery information, including error messages for any failed deliveries. ## Sending Alerts from Hot Code You can publish custom alerts from your Hot code using the `alert` function (auto-imported from `::hot::alert`): ```hot // Send an alert with a payload alert("payment:failed", {"order_id": order-id, "error": err-msg}) // Send an alert without a payload alert("health:degraded") ``` Alerts published from code follow the same routing rules: matching channel patterns trigger deliveries to subscribed destinations. ## Managing Alerts in the Dashboard Access alerts configuration from **Alerts** in the [Hot App](/docs/app) sidebar. The alerts interface has four tabs: - **Destinations** - Configure where alerts are sent - **Subscriptions** - Link alert channels to destinations - **Channels** - View and manage alert event types - **History** - View triggered alerts and their delivery status --- Source: https://hot.dev/docs/box # Containers (Hot Box) Hot Box lets you run Docker/OCI containers from Hot code. Containers execute asynchronously as tasks, returning immediately with a `TaskInfo` while the container runs in the background. ## Overview Use `::hot::box/start` to run arbitrary container images—Python scripts, Node.js tools, system binaries, or any language. Containers are isolated, resource-limited, and billed by Compute Unit Seconds (CUS). See [Container Billing](/docs/box/billing) for CUS details. ## BoxConf Configure a container with `BoxConf`: | Field | Type | Description | |-------|------|-------------| | `image` | `Str` | Docker/OCI image to run (required) | | `size` | `BoxSize` | Size preset (default: `"small"`) | | `script` | `Str` | Shell script to execute (mutually exclusive with `cmd`) | | `cmd` | `Vec` | Command and arguments (optional, uses image default) | | `entrypoint` | `Vec` | Override image entrypoint (e.g. `[""]` to clear it) | | `env` | `Map` | Environment variables (optional) | | `timeout` | `Int` | Timeout in seconds (overrides size default, max: 86400) | | `network` | `Str` | `"internet"` (default) or `"none"` | | `writable` | `Bool` | Writable root filesystem (default: `true`) | | `tmp-size` | `Int` | `/tmp` tmpfs size in MB (overrides size default) | | `disk-size` | `Int` | `/data` writable disk in MB (overrides size default) | | `memory` | `Int` | Container memory in MB (overrides size default) | `script` runs with `set -ex` (trace commands, exit on error). Use `script` for multi-line shell commands; use `cmd` for non-shell executables. Only one of `script` or `cmd` may be specified. ## BoxSize Presets | Size | Memory | CPU | Tmp | Disk | Timeout | CUS Multiplier | |------|--------|-----|-----|------|---------|----------------| | `nano` | 64 MB | 10% | 32 MB | 256 MB | 60s | 0.25x | | `micro` | 128 MB | 25% | 64 MB | 512 MB | 60s | 0.5x | | `small` | 256 MB | 25% | 128 MB | 1 GB | 60s | 1.0x | | `medium` | 512 MB | 50% | 256 MB | 5 GB | 300s | 2.0x | | `large` | 1 GB | 75% | 500 MB | 10 GB | 600s | 4.0x | | `xlarge` | 2 GB | 100% | 1 GB | 20 GB | 1800s | 8.0x | | `2xlarge` | 4 GB | 100% | 2 GB | 50 GB | 3600s | 16.0x | | `4xlarge` | 8 GB | 100% | 4 GB | 50 GB | 7200s | 32.0x | ## Network Access | Value | Description | |-------|-------------| | `"internet"` | Outbound internet access via bridge networking (default) | | `"none"` | No network access | Network access may be restricted by your plan. See [Container Billing](/docs/box/billing) for plan limits. ## Security Model **Default containers** (writable, with internet): - **Writable root** — Root filesystem is writable (for `apk add`, `pip install`, etc.) - **Runs as root** — Process runs as UID 0 with a subset of Linux capabilities - **Process limit** — Maximum 512 processes - **Image denylist** — Dangerous images (e.g. `docker:*`) are blocked - **Internet access** — Outbound network access enabled **Read-only containers** (`writable: false`): - **Read-only root** — Root filesystem is read-only - **Capabilities dropped** — All Linux capabilities are dropped - **Runs as nobody** — Process runs as unprivileged user (UID 65534) - **Process limit** — Maximum 100 processes - **Image denylist** — Same denylist applies Both modes provide `/data` (disk-backed) and `/tmp` (tmpfs) as writable directories. Use `writable: false` for maximum isolation; use `network: "none"` to disable outbound access. ## File Access | Path | Type | Description | |------|------|-------------| | `/data` | Writable disk | Persistent writable storage (size from `disk-size`) | | `/tmp` | tmpfs | Ephemeral tmpfs (size from `tmp-size`) | | `hot://` | Storage | Access Hot storage via built-in file server | ## Image Policy Hot uses an open policy with a denylist. All images are allowed except those matching denied names or prefixes. Images such as `docker:dind` and `docker:*` are blocked. Check the current policy with `::hot::box/image-policy()`: ```hot policy ::hot::box/image-policy() policy.policy // "open" policy.denied // ["docker:dind", ...] policy.denied-prefixes // ["docker:", "rancher/", ...] ``` ## Example ```hot ::box ::hot::box task ::box/start(BoxConf({ image: "python:3.13-alpine", cmd: ["python", "-c", "print('Hello')"], size: "nano" })) // task.id — the task ID // task.stream-id — the stream ID ``` Using `script` for multi-line shell commands: ```hot task ::box/start(BoxConf({ image: "alpine:latest", script: """ echo 'hello from hot box' > /data/test.txt hotbox cp /data/test.txt hot://output/test.txt """, size: "nano", })) ``` With environment variables and custom limits: ```hot task ::box/start(BoxConf({ image: "node:22-alpine", cmd: ["node", "-e", "console.log(process.env.NAME)"], env: {NAME: "Hot"}, size: "medium", })) ``` ## Local Development Running `::hot::box/start` locally requires **Docker** (Docker Desktop or Docker Engine). The `hot dev` command starts a task worker that uses Docker to run containers. If Docker is not installed or not running, `hot dev` will log a warning at startup, and any `::hot::box/start` calls will fail. ```bash # Ensure Docker is running, then: hot dev ``` To disable Hot Box (e.g. if you don't need containers), set `hot.box.enabled` to `false` in your project config or via environment variable: ```bash HOT_BOX_ENABLED=false hot dev ``` ## Useful Functions | Function | Description | |----------|-------------| | `::hot::box/start(BoxConf)` | Start a container task, returns `TaskInfo` | | `::hot::box/sizes()` | Get all size presets with resource profiles | | `::hot::box/quota()` | Check remaining CUS and task quota | | `::hot::box/enabled()` | Check if box is enabled in configuration | | `::hot::box/image-policy()` | Get image denylist and policy | --- Source: https://hot.dev/docs/box/billing # Container Billing (CUS) Container tasks are billed using **Compute Unit Seconds** (CUS), a metric that combines wall-clock time with container size. ## What Are Compute Unit Seconds (CUS) CUS measure container resource usage for billing. The formula: ``` CUS = ceil(wall_clock_seconds × size_multiplier) ``` - **wall_clock_seconds** — Billable execution time (see [What Counts as Billable Time](#what-counts-as-billable-time)) - **size_multiplier** — Multiplier from the container size preset - **ceil** — Rounds up (e.g. 0.5 seconds at 1x = 1 CUS) A 10-second run at `small` (1.0x) consumes 10 CUS. The same run at `nano` (0.25x) consumes 3 CUS. ## What Counts as Billable Time Billable time measures your workload's execution window, not the platform's overhead. The clock starts when your container begins booting and stops when your command exits. **Billed:** - Container boot and runtime initialization - Your command's execution, from entrypoint to exit **Never billed:** - **Image pull** — transferring your image to the worker, even on a cold worker that has never seen it. Identical tasks cost the same whether or not the worker that picked them up already had the image cached. - **Capacity waits** — time spent queued for an execution slot. - **Platform cleanup** — log collection and container removal after your command exits. - **Preparation** — bundle download and extraction for `mounts` entries, and other worker-side setup. The `duration-ms` field on task results and completion events reflects billable time. The timing breakdown on each result reports every phase separately — `image-pull-ms`, `slot-wait-ms`, `runtime-start-ms`, `execution-ms`, `logs-collect-ms` — so you can see exactly what was excluded. **Timeouts are separate from billing.** A container task's `timeout` budget is measured from when a worker claims the task, so it covers preparation phases as well as execution: a slow image pull consumes your task's time budget, but never your CUS. ## CUS Multiplier by Size | Size | Multiplier | |------|------------| | `nano` | 0.25x | | `micro` | 0.5x | | `small` | 1.0x | | `medium` | 2.0x | | `large` | 4.0x | | `xlarge` | 8.0x | | `2xlarge` | 16.0x | | `4xlarge` | 32.0x | Get multipliers programmatically with `::hot::box/sizes()`: ```hot all-sizes ::hot::box/sizes() // [{name: "nano", memory-mb: 64, cus-multiplier: 0.25, ...}, ...] ``` ## Included CUS per Plan | Plan | Included CUS per Month | |------|------------------------| | Free | 5,000 | | Starter | 50,000 | | Pro | 500,000 | | Scale | 5,000,000 | ## Overage Usage beyond included CUS is handled by plan: - **Free plan** — Hard cap. When CUS are exhausted, new container tasks are blocked until the next billing period. - **Paid plans** (Starter, Pro, Scale) — Overage is billed at the per-CUS rate on your next invoice. ## Org Budget Organizations can set an optional **spending cap** (`compute_units_budget`). When reached, container tasks are hard-blocked regardless of plan. This prevents unexpected overage charges. ## Checking Quota Use `::hot::box/quota()` to check remaining CUS before starting containers: ```hot q ::hot::box/quota() q.compute-units-remaining // CUS left this period (-1 = unlimited) q.compute-units-used // CUS consumed this period q.tasks-remaining // Tasks left (-1 = unlimited, 0 = exhausted) q.overage // true if usage exceeds included (paid plans) ``` Example: check quota before running a container: ```hot q ::hot::box/quota() if(eq(q.tasks-remaining, 0), fail("No container tasks remaining"), "OK") if(or(eq(q.compute-units-remaining, -1), gt(q.compute-units-remaining, 0)), ::hot::box/start(BoxConf({image: "alpine", cmd: ["echo", "hello"], size: "nano"})), fail("CUS quota exhausted")) ``` --- Source: https://hot.dev/docs/api # HOT API The Hot API provides programmatic access to manage projects, builds, runs, tasks, events, and more. > **Official SDKs** are available for JavaScript/TypeScript, Python, Go, Rust, > and Java — see [SDKs](api/sdks). The examples on this page show the raw > HTTP API; the SDKs wrap every endpoint below. ## Base URL ``` https://api.hot.dev/v1 ``` For local development: ``` http://localhost:4681/v1 ``` ## Authentication All API requests (except health checks) require a bearer token in the `Authorization` header: ```bash curl https://api.hot.dev/v1/projects \ -H "Authorization: Bearer " ``` Hot supports three credential types — **API keys**, **service keys**, and **sessions** — all used the same way. All credentials are scoped to an environment, and resources are automatically filtered to that environment's context. See the [Authentication](/docs/authentication) documentation for full details on credential types, the permissions model, and the permissions builder. ### Permissions Model {#permissions-model} API keys, service keys, and sessions share a granular permission system. Permissions are a JSON map of resource URNs to action arrays: ```json { "mcp:weather/get-forecast": ["execute"], "stream:*": ["read"], "event:user:*": ["create", "read"] } ``` **Resource URN format:** `type:path` - `type` is the resource category - `path` is the resource identifier (`*` for wildcard) **Resource types and valid actions:** | Resource Type | Valid Actions | Description | |--------------|-------------|-------------| | `mcp` | `execute` | MCP tool invocation (e.g., `mcp:weather/get-forecast`) | | `webhook` | `execute` | Webhook endpoint access (e.g., `webhook:payments/*`) | | `stream` | `read` | Stream subscription (e.g., `stream:*` or `stream:`) | | `event` | `create`, `read` | Event publishing and reading | | `run` | `read` | Run inspection | | `project` | `create`, `read`, `update`, `delete` | Project management | | `build` | `create`, `read`, `execute` | Build management and deployment | | `context` | `create`, `read`, `update`, `delete` | Context variable management | | `key` | `create`, `read`, `update`, `delete` | API key management | | `session` | `create`, `read`, `delete` | Session management | | `env` | `read` | Environment information | The wildcard action `*` grants all valid actions for that resource type. The universal wildcard `*:*` with `["*"]` grants unrestricted access. **Validation Rules:** Permissions are validated when creating or updating API keys, sessions, and service keys. The following rules apply: | Rule | Example (rejected) | Error | |------|--------------------|-------| | Resource must use `type:path` format | `"no-colon-here"` | Invalid resource | | Resource key must not be empty | `""` | Invalid resource | | Type must not be empty | `":path"` | Invalid resource | | Path must not be empty | `"mcp:"` | Invalid resource | | Bare `*` is not valid — use `*:*` | `"*"` | Invalid resource | | `*` type only allows `*` path | `"*:foo"` | Invalid resource | | Type must be alphanumeric/hyphens | `"mcp!:test"` | Invalid resource | | Actions must not be empty | `{"mcp:*": []}` | Empty action list | | Actions are lowercase only | `"Read"`, `"CREATE"` | Invalid action | | Only valid actions: `create`, `read`, `update`, `delete`, `execute`, `*` | `"destroy"` | Invalid action | | Action must be valid for the resource type | `"mcp:*": ["create"]` | Action not valid for resource | Sessions and service keys must also be a **subset** of the parent API key's permissions — you cannot escalate permissions beyond what the issuing key allows. ### Rate Limiting API requests are rate limited per organization. Limits are based on your subscription plan: | Plan | Requests per Second | |------|-------------------| | Starter | 20 RPS | | Pro | 100 RPS | | Scale / Self-Host | Unlimited | When the limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header indicating how many seconds to wait before retrying. ## Response Format All responses use a consistent envelope format. ### Success Response (Single Item) ```json { "data": { "project_id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-project" }, "meta": { "request_id": "123e4567-e89b-12d3-a456-426614174000", "timestamp": "2024-01-15T10:30:00Z" } } ``` ### Success Response (List) ```json { "data": [...], "pagination": { "total": 42, "limit": 20, "offset": 0, "has_more": true }, "meta": { "request_id": "123e4567-e89b-12d3-a456-426614174000", "timestamp": "2024-01-15T10:30:00Z" } } ``` ### Error Response ```json { "error": { "code": "not_found", "message": "Project not found", "request_id": "123e4567-e89b-12d3-a456-426614174000" } } ``` ## Pagination List endpoints support pagination via query parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | int | 20 | Maximum results to return | | `offset` | int | 0 | Number of results to skip | --- ## Endpoints ### Health & Status #### Get API Status ```http GET /status ``` Returns API server health information. No authentication required. Note that `/status` is served at the server root — it is the one endpoint **not** under the `/v1` prefix. **Response:** ```json { "status": "ok", "service": "hot.dev api server", "version": "1.0.0", "git_sha": "abc1234", "start_time": "2026-01-15T10:00:00Z" } ``` --- ### Projects #### List Projects ```http GET /v1/projects ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | **Response:** ```json { "data": [ { "project_id": "550e8400-e29b-41d4-a716-446655440000", "env_id": "660e8400-e29b-41d4-a716-446655440000", "name": "my-project", "active": true, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "pagination": {...}, "meta": {...} } ``` #### Create Project ```http POST /v1/projects ``` **Request Body:** ```json { "name": "my-project" } ``` **Response:** `201 Created` with project data. #### Get Project ```http GET /v1/projects/{project_id_or_slug} ``` Supports both UUID and project name (slug) in the URL. #### Update Project ```http PATCH /v1/projects/{project_id_or_slug} ``` **Request Body:** ```json { "name": "new-project-name" } ``` #### Delete Project ```http DELETE /v1/projects/{project_id_or_slug} ``` **Response:** `204 No Content` --- ### Builds #### List Builds (All in Environment) ```http GET /v1/builds ``` Lists all builds across all projects in the environment. **Response includes** `project_name` for each build. #### List Builds (By Project) ```http GET /v1/projects/{project_id_or_slug}/builds ``` #### Get Build ```http GET /v1/projects/{project_id_or_slug}/builds/{build_id} ``` **Response:** ```json { "data": { "build_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440000", "hash": "abc123def456", "size": 102400, "build_type": "bundle", "deployed": false, "active": true, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "storage_path": "s3://builds/...", "storage_backend": "s3" }, "meta": {...} } ``` #### Get Deployed Build ```http GET /v1/projects/{project_id_or_slug}/builds/deployed ``` Returns the currently deployed build for the project, or `404` if none. #### Get Live Build ```http GET /v1/projects/{project_id_or_slug}/builds/live ``` Returns the live (development) build for the project, or `404` if none. #### Upload Build ```http POST /v1/projects/{project_id_or_slug}/builds Content-Type: multipart/form-data ``` **Form Fields:** | Field | Required | Description | |-------|----------|-------------| | `file` | Yes | The build zip file | | `hash` | Yes | SHA hash of the build for validation | | `build_id` | No | Optional UUID; if provided, enables idempotent uploads | **Examples:** #### **curl** ```bash curl -X POST 'https://api.hot.dev/v1/projects/my-project/builds' \ -H "Authorization: Bearer $HOT_API_KEY" \ -F "file=@build.hot.zip" \ -F "hash=$(sha256sum build.hot.zip | cut -d' ' -f1)" ``` #### **JavaScript** ```javascript const fs = require('fs'); const crypto = require('crypto'); const FormData = require('form-data'); const file = fs.readFileSync('build.hot.zip'); const hash = crypto.createHash('sha256').update(file).digest('hex'); const form = new FormData(); form.append('file', file, 'build.hot.zip'); form.append('hash', hash); const response = await fetch(`${BASE_URL}/projects/my-project/builds`, { method: 'POST', headers: { 'Authorization': `Bearer ${HOT_API_KEY}` }, body: form }); ``` #### **Python** ```python import hashlib with open('build.hot.zip', 'rb') as f: file_hash = hashlib.sha256(f.read()).hexdigest() response = requests.post( f'{BASE_URL}/projects/my-project/builds', headers={'Authorization': f'Bearer {HOT_API_KEY}'}, files={'file': open('build.hot.zip', 'rb')}, data={'hash': file_hash} ) ``` **Response:** `201 Created` ```json { "data": { "build_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440000", "hash": "abc123def456", "size": 102400, "storage_path": "s3://builds/...", "storage_backend": "s3", "created_at": "2024-01-15T10:30:00Z" }, "meta": {...} } ``` If the `build_id` already exists, returns `200 OK` with header `X-Build-Exists: true`. #### Download Build ```http GET /v1/projects/{project_id_or_slug}/builds/{build_id}/download ``` Returns the build as a zip file with `Content-Type: application/zip`. #### Deploy Build ```http POST /v1/projects/{project_id_or_slug}/builds/{build_id}/deploy ``` Marks the build as deployed and queues it for worker processing. --- ### Files Files are stored per-environment and can be managed through the API. Small files (up to 300 MB) can be uploaded in a single request. For larger files, use the multipart upload flow. **Per-plan size limits:** | Plan | Max File Upload | |------|-----------------| | Free | 100 MB | | Starter | 1 GB | | Pro | 5 GB | | Scale | 50 GB | | Self-Host | 50 GB | #### List Files ```http GET /v1/files ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `prefix` | string | Filter files by path prefix | | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | **Response:** ```json { "data": [ { "file_id": "550e8400-e29b-41d4-a716-446655440000", "path": "uploads/report.pdf", "size": 102400, "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "content_type": "application/pdf", "storage_backend": "s3", "created_by_run_id": null, "updated_by_run_id": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "pagination": {"total": 1, "limit": 20, "offset": 0}, "meta": {...} } ``` #### Get File Metadata ```http GET /v1/files/{file_id} ``` Returns metadata for a single file. #### Download File ```http GET /v1/files/{file_id}/download ``` Returns the file content as a binary download with appropriate `Content-Type` and `Content-Disposition` headers. #### Upload File (Simple) ```http PUT /v1/files/upload/{path} Content-Type: application/octet-stream ``` Upload a file in a single request. The request body is the raw file content. Suitable for files up to 300 MB (the HTTP body limit). For larger files, use the multipart upload flow below. #### **curl** ```bash curl -X PUT 'https://api.hot.dev/v1/files/upload/data/report.csv' \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: text/csv" \ --data-binary @report.csv ``` #### **JavaScript** ```javascript const file = fs.readFileSync('report.csv'); const response = await fetch(`${BASE_URL}/files/upload/data/report.csv`, { method: 'PUT', headers: { 'Authorization': `Bearer ${HOT_API_KEY}`, 'Content-Type': 'text/csv' }, body: file }); ``` #### **Python** ```python with open('report.csv', 'rb') as f: response = requests.put( f'{BASE_URL}/files/upload/data/report.csv', headers={ 'Authorization': f'Bearer {HOT_API_KEY}', 'Content-Type': 'text/csv' }, data=f ) ``` **Response:** `200 OK` ```json { "data": { "file_id": "550e8400-e29b-41d4-a716-446655440000", "path": "data/report.csv", "size": 102400, "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "content_type": "text/csv", "storage_backend": "s3", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" }, "meta": {...} } ``` #### Delete File ```http DELETE /v1/files/{file_id} ``` Returns `204 No Content` on success. #### Multipart Upload For files larger than 300 MB (or when you want resumable uploads), use the three-step multipart flow: initiate, upload parts, then complete. ##### 1. Initiate Upload ```http POST /v1/files/uploads Content-Type: application/json ``` **Request Body:** | Field | Required | Description | |-------|----------|-------------| | `path` | Yes | Destination file path | | `expected_size` | No | Expected total size in bytes (enables quota pre-check and determines part count) | | `content_type` | No | MIME type of the file | **Example:** ```json { "path": "data/large-dataset.parquet", "expected_size": 1073741824, "content_type": "application/octet-stream" } ``` **Response:** `201 Created` ```json { "data": { "upload_id": "660e8400-e29b-41d4-a716-446655440000", "path": "data/large-dataset.parquet", "part_size": 67108864, "parts_expected": 16, "expires_at": "2024-01-16T10:30:00Z" }, "meta": {...} } ``` The `part_size` (in bytes) is the recommended size for each part. Use this value when splitting your file. ##### 2. Upload Parts ```http PUT /v1/files/uploads/{upload_id}/{part_number} Content-Type: application/octet-stream ``` Upload each part as raw bytes. Part numbers are 1-based. **Constraints:** - Non-final parts must be at least 5 MB - Maximum part size is 256 MB - Maximum 10,000 parts per upload **Response:** ```json { "data": { "part_number": 1, "size": 67108864, "etag": "\"a54357aff0632cce46d942af68356b38\"" }, "meta": {...} } ``` ##### 3. Complete Upload ```http POST /v1/files/uploads/{upload_id}/complete ``` Assembles all uploaded parts into the final file. Returns the file metadata (same shape as simple upload response). ##### Abort Upload ```http DELETE /v1/files/uploads/{upload_id} ``` Cancels the upload and cleans up any uploaded parts. Returns `204 No Content`. #### **curl** ```bash # 1. Initiate UPLOAD=$(curl -s -X POST 'https://api.hot.dev/v1/files/uploads' \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "data/large-file.bin", "expected_size": 134217728}') UPLOAD_ID=$(echo $UPLOAD | jq -r '.data.upload_id') PART_SIZE=$(echo $UPLOAD | jq -r '.data.part_size') # 2. Upload parts (split file and upload each chunk) split -b $PART_SIZE large-file.bin part_ PART_NUM=1 for part in part_*; do curl -X PUT "https://api.hot.dev/v1/files/uploads/$UPLOAD_ID/$PART_NUM" \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @$part PART_NUM=$((PART_NUM + 1)) done # 3. Complete curl -X POST "https://api.hot.dev/v1/files/uploads/$UPLOAD_ID/complete" \ -H "Authorization: Bearer $HOT_API_KEY" ``` #### **JavaScript** ```javascript // 1. Initiate const initRes = await fetch(`${BASE_URL}/files/uploads`, { method: 'POST', headers: { 'Authorization': `Bearer ${HOT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ path: 'data/large-file.bin', expected_size: fileBuffer.length }) }); const { data: { upload_id, part_size } } = await initRes.json(); // 2. Upload parts for (let i = 0; i < fileBuffer.length; i += part_size) { const partNum = Math.floor(i / part_size) + 1; const chunk = fileBuffer.slice(i, i + part_size); await fetch(`${BASE_URL}/files/uploads/${upload_id}/${partNum}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${HOT_API_KEY}`, 'Content-Type': 'application/octet-stream' }, body: chunk }); } // 3. Complete await fetch(`${BASE_URL}/files/uploads/${upload_id}/complete`, { method: 'POST', headers: { 'Authorization': `Bearer ${HOT_API_KEY}` } }); ``` #### **Python** ```python import math # 1. Initiate file_size = os.path.getsize('large-file.bin') init = requests.post( f'{BASE_URL}/files/uploads', headers={'Authorization': f'Bearer {HOT_API_KEY}'}, json={'path': 'data/large-file.bin', 'expected_size': file_size} ).json() upload_id = init['data']['upload_id'] part_size = init['data']['part_size'] # 2. Upload parts with open('large-file.bin', 'rb') as f: for part_num in range(1, math.ceil(file_size / part_size) + 1): chunk = f.read(part_size) requests.put( f'{BASE_URL}/files/uploads/{upload_id}/{part_num}', headers={ 'Authorization': f'Bearer {HOT_API_KEY}', 'Content-Type': 'application/octet-stream' }, data=chunk ) # 3. Complete requests.post( f'{BASE_URL}/files/uploads/{upload_id}/complete', headers={'Authorization': f'Bearer {HOT_API_KEY}'} ) ``` --- ### Context Variables (Secrets) Context variables are encrypted secrets stored per-project. Values are encrypted at rest using AES-256-GCM. **Security:** Values are never returned via API—only metadata (key, description, timestamps). #### List Context Variables ```http GET /v1/projects/{project_id_or_slug}/context ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | **Response:** ```json { "data": [ { "key": "DATABASE_URL", "description": "Production database connection", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "pagination": {...}, "meta": {...} } ``` #### Create Context Variable ```http POST /v1/projects/{project_id_or_slug}/context ``` **Request Body:** ```json { "key": "DATABASE_URL", "value": "postgres://user:pass@host/db", "description": "Production database connection" } ``` **Response:** `201 Created` (value is not returned). #### Update Context Variable ```http PUT /v1/projects/{project_id_or_slug}/context/{key} ``` **Request Body:** ```json { "value": "postgres://user:newpass@host/db", "description": "Updated description" } ``` #### Delete Context Variable ```http DELETE /v1/projects/{project_id_or_slug}/context/{key} ``` **Response:** `204 No Content` --- ### Events #### Publish Event ```http POST /v1/events ``` Publishes an event that can trigger event handlers. `event_type` is an arbitrary string chosen by your application (for example `user:created`). Hot does not enforce a specific naming pattern, but `:`-separated names are the recommended convention. For comparison: - Hot language `send(...)` uses `type` and `data` - HTTP API `POST /v1/events` uses `event_type` and `event_data` **Request Body:** ```json { "event_type": "user:signup", "event_data": { "user_id": "123", "email": "alice@example.com" } } ``` **Response:** `201 Created` ```json { "data": { "event_id": "550e8400-e29b-41d4-a716-446655440000", "env_id": "660e8400-e29b-41d4-a716-446655440000", "stream_id": "770e8400-e29b-41d4-a716-446655440000", "event_type": "user:signup", "event_data": {...}, "event_time": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z" }, "meta": {...} } ``` #### List Events ```http GET /v1/events ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | #### Get Event ```http GET /v1/events/{event_id} ``` #### Get Runs for Event ```http GET /v1/events/{event_id}/runs ``` Returns all runs triggered by this event. --- ### Runs #### List Runs ```http GET /v1/runs ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | | `status` | string | Filter: `running`, `succeeded`, `failed`, `cancelled`, `pending_retry` | | `type` | string | Filter: `call`, `event`, `schedule`, `run`, `eval`, `repl` | | `time_range` | string | ISO 8601 duration: `P7D`, `P30D`, etc. | **Response:** ```json { "data": [ { "run_id": "550e8400-e29b-41d4-a716-446655440000", "env_id": "660e8400-e29b-41d4-a716-446655440000", "stream_id": "770e8400-e29b-41d4-a716-446655440000", "build_id": "880e8400-e29b-41d4-a716-446655440000", "run_type": "event", "status": "succeeded", "start_time": "2024-01-15T10:30:00Z", "stop_time": "2024-01-15T10:30:45Z", "origin_run_id": null, "event_id": "990e8400-e29b-41d4-a716-446655440000", "result": {...}, "project_id": "aa0e8400-e29b-41d4-a716-446655440000", "project_name": "my-project" } ], "pagination": {...}, "meta": {...} } ``` #### Get Run ```http GET /v1/runs/{run_id} ``` #### Subscribe to Run ```http GET /v1/runs/{run_id}/subscribe Accept: text/event-stream ``` The response is an SSE stream of durable `run:update` snapshots. The first event is always the latest persisted run, so subscribing after completion is safe. The stream reports subsequent changes and closes after `succeeded`, `failed`, or `cancelled`; `pending_retry` remains non-terminal. ```text event: run:update data: {"type":"run:update","run":{"run_id":"...","status":"running",...}} event: run:update data: {"type":"run:update","run":{"run_id":"...","status":"succeeded","result":{...},...}} ``` Clients should reconnect to this endpoint after an interrupted connection. Official SDK run waiters handle that automatically. #### Get Run Statistics ```http GET /v1/runs/stats ``` **Response:** ```json { "data": { "total_runs": 1234, "running": 5, "succeeded": 1200, "failed": 25, "cancelled": 4 }, "meta": {...} } ``` --- ### Tasks Tasks are asynchronous code or container executions. Use the task endpoints when a run returns a task id and the client needs to follow that work without keeping the originating run open. #### Get Task ```http GET /v1/tasks/{task_id} ``` Returns the latest persisted task snapshot. Task status is one of `queued`, `running`, `completed`, `failed`, `cancelled`, or `timed_out`. ```json { "data": { "task_id": "550e8400-e29b-41d4-a716-446655440000", "env_id": "660e8400-e29b-41d4-a716-446655440000", "stream_id": "770e8400-e29b-41d4-a716-446655440000", "build_id": "880e8400-e29b-41d4-a716-446655440000", "run_id": null, "origin_run_id": "990e8400-e29b-41d4-a716-446655440000", "function_name": "::myapp::jobs/render", "task_type": "code", "status": "completed", "start_time": "2024-01-15T10:30:00Z", "stop_time": "2024-01-15T10:31:20Z", "duration_ms": 80000, "result": {"asset_id": "asset_123"}, "timeout_ms": 3600000, "retry_attempt": 0, "next_retry_at": null, "created_at": "2024-01-15T10:29:59Z" }, "meta": {...} } ``` #### Subscribe to Task ```http GET /v1/tasks/{task_id}/subscribe Accept: text/event-stream ``` The response is an SSE stream of `task:update` events. The first event is always the latest persisted task snapshot, even if the task was already terminal before the connection opened. Subsequent events report state changes, and the stream closes after a terminal snapshot. ```text event: task:update data: {"type":"task:update","task":{"task_id":"...","status":"running",...}} event: task:update data: {"type":"task:update","task":{"task_id":"...","status":"completed","result":{...},...}} ``` Clients should reconnect to the same endpoint after an interrupted connection; the persisted first snapshot makes reconnection race-free. The official SDK task waiters implement this behavior. --- ### Event Handlers Event handlers are registered in your Hot code and loaded when builds are uploaded. #### List Event Handlers ```http GET /v1/projects/{project_id_or_slug}/event-handlers ``` Returns event handlers from the project's deployed build. **Response:** ```json { "data": [ { "event_handler_id": "550e8400-e29b-41d4-a716-446655440000", "build_id": "660e8400-e29b-41d4-a716-446655440000", "event_type": "user:signup", "ns": "::myapp::handlers", "var": "on-user-signup" } ], "pagination": {...}, "meta": {...} } ``` --- ### Schedules Schedules are cron-based triggers defined in your Hot code. #### List Schedules ```http GET /v1/projects/{project_id_or_slug}/schedules ``` Returns schedules from the project's deployed build. **Response:** ```json { "data": [ { "schedule_id": "550e8400-e29b-41d4-a716-446655440000", "build_id": "660e8400-e29b-41d4-a716-446655440000", "cron": "0 0 * * *", "ns": "::myapp::tasks", "var": "daily-cleanup" } ], "pagination": {...}, "meta": {...} } ``` --- ### Environment #### Get Environment Info ```http GET /v1/env ``` **Response:** ```json { "data": { "env_id": "550e8400-e29b-41d4-a716-446655440000", "org_id": "660e8400-e29b-41d4-a716-446655440000", "name": "production", "active": true }, "meta": {...} } ``` #### Subscribe to Environment Events (SSE) ```http GET /v1/env/subscribe ``` Subscribe to real-time events for the entire environment via Server-Sent Events (SSE). This endpoint streams all run, event, and stream activity for the environment associated with your API key. **Response:** `text/event-stream` **SSE Event Types:** | Event | Description | |-------|-------------| | `run:start` | A new run has started | | `run:stop` | A run completed successfully | | `run:fail` | A run failed | | `run:cancel` | A run was cancelled | | `event:created` | A new event was created | | `event:handled` | An event was handled | | `stream:created` | A new stream was created | **Example Events:** ``` event: run:start data: {"run_id":"550e8400-...","stream_id":"660e8400-...","run_type":"event"} event: run:stop data: {"run_id":"550e8400-...","stream_id":"660e8400-..."} event: event:created data: {"event_id":"770e8400-...","stream_id":"880e8400-...","event_type":"user:signup"} ``` **Examples:** #### **curl** ```bash curl -N 'https://api.hot.dev/v1/env/subscribe' \ -H "Authorization: Bearer $HOT_API_KEY" ``` > Note: `-N` disables buffering for real-time streaming output. #### **JavaScript** ```javascript // Note: the browser-native EventSource API can't send an Authorization // header, so use fetch with a stream reader instead. const response = await fetch('https://api.hot.dev/v1/env/subscribe', { headers: { 'Authorization': `Bearer ${HOT_API_KEY}`, 'Accept': 'text/event-stream' } }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); for (const line of text.split('\n')) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); console.log('SSE event:', data); } } } ``` #### **Python** ```python import requests import json response = requests.get( 'https://api.hot.dev/v1/env/subscribe', headers={'Authorization': f'Bearer {HOT_API_KEY}'}, stream=True ) for line in response.iter_lines(): if line: line = line.decode('utf-8') if line.startswith('data: '): event = json.loads(line[6:]) print(f"Event: {event}") ``` **Notes:** - The stream automatically times out after 5 minutes. Reconnect to continue receiving events. - Events are scoped to the environment associated with your API key. - This endpoint requires pub/sub to be configured on the server. --- ### Organization #### Get Usage & Limits ```http GET /v1/org/usage ``` Returns current usage statistics, plan limits, and usage percentages for the organization. **Response:** ```json { "data": { "org_id": "660e8400-e29b-41d4-a716-446655440000", "usage": { "runs_this_period": 1250, "file_storage_bytes": 52428800, "team_members": 5, "call_storage_bytes": 104857600, "call_count": 15000 }, "limits": { "runs_per_month": 10000, "storage_bytes": 1073741824, "team_members": 10, "call_retention_days": 30, "call_storage_bytes": 5368709120 }, "usage_percent": { "runs": 12.5, "file_storage": 4.9, "team_members": 50.0, "call_storage": 1.95, "has_warning": false }, "plan": { "name": "Pro", "period_start": "2024-01-01T00:00:00Z", "period_end": "2024-02-01T00:00:00Z" } }, "meta": {...} } ``` **Fields:** | Field | Description | |-------|-------------| | `usage` | Current usage in the billing period | | `limits` | Plan limits (-1 = unlimited) | | `usage_percent` | Usage as percentage of limits (can exceed 100 if over limit) | | `usage_percent.has_warning` | True if any usage exceeds 90% | | `plan.period_start` | Start of current billing period | | `plan.period_end` | End of current billing period | For self-hosted/local deployments without a subscription, all limits are unlimited (-1). --- ### Streams (Server-Sent Events) Streams provide real-time execution updates via Server-Sent Events (SSE). A stream groups related events, runs, and tasks together, allowing you to track the full lifecycle of an operation. #### Subscribe to Stream ```http GET /v1/streams/{stream_id}/subscribe ``` Subscribe to an existing stream to receive real-time updates. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `project` | string | Optional project filter | **Response:** `text/event-stream` **SSE Event Types:** | Event | Description | |-------|-------------| | `run:start` | A new run has started | | `run:stop` | A run completed successfully | | `run:fail` | A run failed | | `run:cancel` | A run was cancelled | | `task:update` | Latest persisted state of a task belonging to this stream | | `stream:data` | Real-time data from the run (e.g., AI tokens) | | `stream:complete` | Stream subscription ended — the stream completed or the subscription timed out (5 minute default) | **Example Event:** ``` event: run:start data: {"type":"run:start","run":{"run_id":"...","status":"running",...}} event: stream:data data: {"type":"stream:data","run_id":"...","data_type":"ai:delta","payload":{"text":"Hello"}} event: run:stop data: {"type":"run:stop","run":{"run_id":"...","status":"succeeded","result":"Hello world"}} event: task:update data: {"type":"task:update","task":{"task_id":"...","status":"running",...}} ``` On connection, Hot sends the latest persisted snapshot for tasks already on the stream, then sends another `task:update` when a task changes. This makes a single stream subscription useful for coordinating several tasks. Use the task-specific subscription when the client only needs one task and wants the connection to close automatically at that task's terminal state. #### Subscribe with Event (Atomic) ```http POST /v1/streams/subscribe-with-event Accept: text/event-stream ``` **Recommended for streaming use cases.** This endpoint atomically subscribes to a stream AND publishes an event in a single request, eliminating race conditions where events might be missed. **Request Body:** ```json { "event_type": "chat:message", "event_data": { "message": "Hello, world!", "history": [] }, "stream_id": "optional-existing-stream-uuid" } ``` | Field | Required | Description | |-------|----------|-------------| | `event_type` | Yes | The event type to publish | | `event_data` | Yes | Event payload (any JSON) | | `stream_id` | No | Continue an existing stream; if omitted, creates a new stream | **Response:** `text/event-stream` The first event is always `event:published` confirming the event was queued: ``` event: event:published data: {"type":"event:published","event_id":"...","stream_id":"...","event_type":"chat:message"} event: run:start data: {"type":"run:start","run":{...}} event: stream:data data: {"type":"stream:data","run_id":"...","data_type":"ai:delta","payload":{"text":"Hello"}} event: run:stop data: {"type":"run:stop","run":{...}} ``` **Examples:** #### **curl** ```bash curl -N -X POST 'https://api.hot.dev/v1/streams/subscribe-with-event' \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"event_type": "chat:message", "event_data": {"message": "Hello!"}}' ``` > Note: `-N` disables buffering for real-time streaming output. #### **JavaScript** ```javascript const response = await fetch('https://api.hot.dev/v1/streams/subscribe-with-event', { method: 'POST', headers: { 'Authorization': `Bearer ${HOT_API_KEY}`, 'Content-Type': 'application/json', 'Accept': 'text/event-stream', }, body: JSON.stringify({ event_type: 'chat:message', event_data: { message: 'Hello!', history: [] } }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // Parse SSE events from chunk for (const line of chunk.split('\n')) { if (line.startsWith('data: ')) { const event = JSON.parse(line.slice(6)); console.log(event.type, event); } } } ``` #### **Python** ```python import requests import json response = requests.post( 'https://api.hot.dev/v1/streams/subscribe-with-event', headers={ 'Authorization': f'Bearer {HOT_API_KEY}', 'Content-Type': 'application/json', 'Accept': 'text/event-stream', }, json={ 'event_type': 'chat:message', 'event_data': {'message': 'Hello!', 'history': []} }, stream=True # Required for SSE ) for line in response.iter_lines(): if line: line = line.decode('utf-8') if line.startswith('data: '): event = json.loads(line[6:]) print(event['type'], event) ``` --- ### Sessions Sessions are short-lived, permission-scoped tokens for ephemeral access. Only API keys can create sessions. #### Create Session ```http POST /v1/sessions ``` **Request Body:** ```json { "permissions": { "stream:*": ["read"], "event:user:*": ["create"] }, "metadata": { "user_id": "end-user-123", "purpose": "stream-subscription" }, "expires_in": 3600 } ``` | Field | Required | Description | |-------|----------|-------------| | `permissions` | Yes | Permission map (resource URN → action array). Must be a subset of the parent API key's permissions. | | `metadata` | No | Arbitrary JSON metadata (user ID, purpose, etc.) | | `expires_in` | No | TTL in seconds (default: 3600, max: 86400) | **Response:** `201 Created` ```json { "data": { "session_id": "550e8400-e29b-41d4-a716-446655440000", "token": "s_0193a7b212347def8abc123456789012_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "permissions": {"stream:*": ["read"], "event:user:*": ["create"]}, "metadata": {"user_id": "end-user-123"}, "expires_at": "2026-01-15T11:30:00Z", "created_at": "2026-01-15T10:30:00Z" }, "meta": {...} } ``` > **Important:** The `token` field is only returned at creation time. Store it securely — it cannot be retrieved later. **Errors:** | Code | Status | Cause | |------|--------|-------| | `forbidden` | 403 | Non-API-key credential attempted to create a session | | `permission_escalation` | 403 | Requested permissions exceed parent API key permissions | | `session_limit_exceeded` | 429 | Maximum active sessions (1000) reached for this API key | #### List Sessions ```http GET /v1/sessions ``` Lists active (non-expired, non-revoked) sessions for the authenticated API key. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | #### Revoke Session ```http DELETE /v1/sessions/{session_id} ``` Revokes a specific session. The session must belong to the authenticated API key. **Response:** `204 No Content` #### Revoke All Sessions ```http DELETE /v1/sessions ``` Revokes all active sessions for the authenticated API key. **Response:** ```json { "data": { "revoked_count": 5 }, "meta": {...} } ``` --- ### Service Keys Service keys are long-lived, permission-scoped credentials you issue to your customers or external systems for access to MCP tools, webhooks, and other API resources. Only API keys can create service keys. #### Create Service Key ```http POST /v1/service-keys ``` **Request Body:** ```json { "name": "Acme Corp Production Key", "description": "MCP and stream access for Acme Corp", "permissions": { "mcp:weather/*": ["execute"], "stream:*": ["read"] }, "metadata": { "customer_id": "acme-123" }, "expires_in": null } ``` | Field | Required | Description | |-------|----------|-------------| | `name` | No | Human-readable name | | `description` | No | Description of the key's purpose | | `permissions` | Yes | Permission map. Must be a subset of the parent API key's permissions. | | `metadata` | No | Arbitrary JSON metadata (encrypted at rest, available at runtime via `req.auth.service-key.meta`) | | `expires_in` | No | TTL in seconds (`null` or omitted = never expires) | **Response:** `201 Created` ```json { "data": { "service_key_id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp Production Key", "description": "MCP and stream access for Acme Corp", "token": "0193a7b212347def8abc123456789012_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "permissions": {"mcp:weather/*": ["execute"], "stream:*": ["read"]}, "metadata": {"customer_id": "acme-123"}, "created_at": "2026-01-15T10:30:00Z" }, "meta": {...} } ``` > **Important:** The `token` field is only returned at creation time. Store it securely — it cannot be retrieved later. Note that service key tokens have no `hot_` prefix, making them suitable for white-label use. **Errors:** | Code | Status | Cause | |------|--------|-------| | `forbidden` | 403 | Non-API-key credential attempted to create a service key | | `permission_escalation` | 403 | Requested permissions exceed parent API key permissions | #### List Service Keys ```http GET /v1/service-keys ``` Lists service keys for the authenticated API key. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Max results (default: 20) | | `offset` | int | Pagination offset | #### Get Service Key ```http GET /v1/service-keys/{service_key_id} ``` Returns details for a specific service key. The key must belong to the authenticated API key. #### Revoke Service Key ```http DELETE /v1/service-keys/{service_key_id} ``` Revokes a specific service key. The key must belong to the authenticated API key. **Response:** `204 No Content` #### Revoke All Service Keys ```http DELETE /v1/service-keys ``` Revokes all active service keys for the authenticated API key. **Response:** ```json { "data": { "revoked_count": 3 }, "meta": {...} } ``` --- ### Custom Domains Custom domains map your own domain names (e.g., `mcp.example.com`) to your Hot Dev environment. This feature requires a **Pro or Scale** subscription plan. #### Register Domain ```http POST /v1/domains ``` **Request Body:** ```json { "domain": "mcp.example.com" } ``` **Response:** `201 Created` ```json { "data": { "domain_id": "550e8400-e29b-41d4-a716-446655440000", "env_id": "660e8400-e29b-41d4-a716-446655440000", "domain": "mcp.example.com", "status": "pending_validation", "validation_cname_name": "_abc123.mcp.example.com", "validation_cname_value": "_xyz789.acm-validations.aws", "routing_domain": null, "created_at": "2026-01-15T10:30:00Z" }, "meta": {...} } ``` After creating a domain, add the **validation CNAME** record (using the `validation_cname_name` and `validation_cname_value` fields) to prove domain ownership. Once validated, the `routing_domain` field will be populated — add a domain CNAME pointing to that routing target to start routing traffic. Domain statuses: `pending_validation`, `validated`, `provisioning`, `active`, `deleting`. **Errors:** | Code | Status | Cause | |------|--------|-------| | `plan_required` | 403 | Custom domains require Pro or Scale plan | | `domain_limit_reached` | 403 | Domain count limit reached for current plan | | `domain_exists` | 409 | Domain is already registered | #### List Domains ```http GET /v1/domains ``` Lists all custom domains for the environment. #### Get Domain ```http GET /v1/domains/{domain_id} ``` #### Verify Domain ```http POST /v1/domains/{domain_id}/verify ``` Checks the current provisioning status of the domain. If the validation CNAME has propagated and the certificate is issued, the domain moves to `validated` status and routing provisioning begins. If not yet validated, returns the required DNS records. Pending domains are also checked automatically in the background, so you don't need to call this endpoint repeatedly. **Response (validated):** ```json { "data": { "domain_id": "550e8400-e29b-41d4-a716-446655440000", "domain": "mcp.example.com", "status": "validated", "message": "Domain validated successfully — routing provisioning in progress" }, "meta": {...} } ``` **Response (pending):** ```json { "data": { "domain_id": "550e8400-e29b-41d4-a716-446655440000", "domain": "mcp.example.com", "status": "pending_validation", "message": "Add a CNAME record: _abc123.mcp.example.com → _xyz789.acm-validations.aws" }, "meta": {...} } ``` #### Delete Domain ```http DELETE /v1/domains/{domain_id} ``` Removes a custom domain. The domain must belong to the authenticated environment. Deletion is asynchronous — the domain enters a `deleting` state while its routing target and TLS certificate are cleaned up, then the record is removed. **Response:** `204 No Content` --- ## Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `unauthorized` | 401 | Invalid, missing, expired, or revoked credential | | `forbidden` | 403 | Credential lacks required permissions | | `permission_escalation` | 403 | Requested permissions exceed parent credential permissions | | `plan_required` | 403 | Feature requires a higher subscription plan | | `not_found` | 404 | Resource not found | | `bad_request` | 400 | Invalid request body or parameters | | `domain_exists` | 409 | Custom domain is already registered | | `domain_limit_reached` | 403 | Domain count limit reached for current plan | | `session_limit_exceeded` | 429 | Maximum active sessions reached for this API key | | `rate_limit_exceeded` | 429 | Too many requests (see `Retry-After` header) | | `internal_server_error` | 500 | Server error | --- ## Code Examples The [official SDKs](api/sdks) cover every endpoint on this page. Listing projects and publishing an event in each: #### **curl** ```bash # List projects curl https://api.hot.dev/v1/projects \ -H "Authorization: Bearer $HOT_API_KEY" # Publish an event curl -X POST https://api.hot.dev/v1/events \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_type": "user:signup", "event_data": {"user_id": "123"}}' ``` #### **JavaScript** ```javascript import { HotClient } from "@hot-dev/sdk"; const hot = new HotClient({ token: process.env.HOT_API_KEY }); // List projects const { data: projects } = await hot.projects.list(); // Publish an event const event = await hot.events.publish({ event_type: "user:signup", event_data: { user_id: "123", email: "alice@example.com" }, }); console.log(event.stream_id); ``` #### **Python** ```python import os from hot import HotClient hot = HotClient(token=os.environ["HOT_API_KEY"]) # List projects projects = hot.projects.list()["data"] # Publish an event event = hot.events.publish( { "event_type": "user:signup", "event_data": {"user_id": "123", "email": "alice@example.com"}, } ) print(event["stream_id"]) ``` #### **Go** ```go client, err := hot.NewClient(hot.Config{Token: os.Getenv("HOT_API_KEY")}) if err != nil { log.Fatal(err) } ctx := context.Background() // List projects projects, err := client.Projects.List(ctx, nil) // Publish an event event, err := client.Events.Publish(ctx, map[string]any{ "event_type": "user:signup", "event_data": map[string]any{"user_id": "123", "email": "alice@example.com"}, }) fmt.Println(event["stream_id"]) ``` #### **Rust** ```rust use hot_dev::HotClient; use serde_json::json; let client = HotClient::builder(std::env::var("HOT_API_KEY").unwrap()).build(); // List projects let projects = client.projects().list(&[]).await?; // Publish an event let event = client .events() .publish(json!({ "event_type": "user:signup", "event_data": { "user_id": "123", "email": "alice@example.com" }, })) .await?; println!("{}", event["stream_id"]); ``` #### **Java** ```java HotClient client = HotClient.builder(System.getenv("HOT_API_KEY")).build(); // List projects Map projects = client.projects().list(); // Publish an event Map event = client.events().publish(Map.of( "event_type", "user:signup", "event_data", Map.of("user_id", "123", "email", "alice@example.com"))); System.out.println(event.get("stream_id")); ``` See [SDKs](api/sdks) for installation, streaming, error handling, and the full behavior shared across all five libraries. --- Source: https://hot.dev/docs/api/sdks # SDKs Official client libraries for the Hot API, released in lockstep versions: | Language | Package | Source | API Reference | |----------|---------|--------|---------------| | JavaScript / TypeScript | [`@hot-dev/sdk`](https://www.npmjs.com/package/@hot-dev/sdk) (npm) | [hot-dev/hot-js](https://github.com/hot-dev/hot-js) | [Package README](https://github.com/hot-dev/hot-js/tree/main/packages/sdk) | | Python | [`hot-dev`](https://pypi.org/project/hot-dev/) (PyPI) | [hot-dev/hot-python](https://github.com/hot-dev/hot-python) | [README](https://github.com/hot-dev/hot-python) | | Go | `github.com/hot-dev/hot-go` | [hot-dev/hot-go](https://github.com/hot-dev/hot-go) | [pkg.go.dev](https://pkg.go.dev/github.com/hot-dev/hot-go) | | Rust | [`hot-dev`](https://crates.io/crates/hot-dev) (crates.io) | [hot-dev/hot-rust](https://github.com/hot-dev/hot-rust) | [docs.rs](https://docs.rs/hot-dev) | | Java | `dev.hot:hot-sdk` (Maven Central) | [hot-dev/hot-java](https://github.com/hot-dev/hot-java) | [javadoc.io](https://javadoc.io/doc/dev.hot/hot-sdk) | Every SDK covers the full API v1 surface: the thirteen resources ([Endpoints](api)), SSE run-stream subscriptions with automatic reconnection across the API's 5-minute stream timeout, durable run and task waiters, structured API errors, and escape hatches for endpoints that do not yet have a helper. Authenticated clients should run server-side. Browser apps and untrusted clients should call your own backend route instead of exposing a Hot API key (the JavaScript SDK ships a `@hot-dev/sdk/proxy` helper for this). ## Install #### **JavaScript** ```bash npm install @hot-dev/sdk ``` Requires Node 20+. ESM-only. #### **Python** ```bash pip install hot-dev ``` Python 3.10+. Import as `hot`. #### **Go** ```bash go get github.com/hot-dev/hot-go ``` Go 1.23+. Zero dependencies. #### **Rust** ```bash cargo add hot-dev tokio --features tokio/full cargo add futures-util serde_json ``` Async on tokio; TLS via rustls. Import as `hot_dev`. #### **Java** ```kotlin // Gradle implementation("dev.hot:hot-sdk:1.1.3") ``` ```xml dev.hot hot-sdk 1.1.3 ``` Java 17+. Jackson is the only runtime dependency. ## Quick Start Publish an event and stream its run to completion. `base_url` defaults to `https://api.hot.dev`; for local development with `hot dev`, point it at `http://localhost:4681`. #### **JavaScript** ```javascript import { HotClient } from "@hot-dev/sdk"; const hot = new HotClient({ token: process.env.HOT_API_KEY }); for await (const event of hot.streams.subscribeWithEvent({ event_type: "team-agent:ask", event_data: { question: "what is blocking launch?" }, })) { if (event.type === "stream:data") console.log(event.data_type, event.payload); if (event.type === "run:stop") { console.log(event.run?.result); break; } } ``` #### **Python** ```python import os from hot import HotClient hot = HotClient(token=os.environ["HOT_API_KEY"]) for event in hot.streams.subscribe_with_event( {"event_type": "team-agent:ask", "event_data": {"question": "what is blocking launch?"}} ): if event["type"] == "stream:data": print(event["data_type"], event.get("payload")) if event["type"] == "run:stop": print(event.get("run", {}).get("result")) break ``` #### **Go** ```go client, err := hot.NewClient(hot.Config{Token: os.Getenv("HOT_API_KEY")}) if err != nil { log.Fatal(err) } ctx := context.Background() for event, err := range client.Streams.SubscribeWithEvent(ctx, map[string]any{ "event_type": "team-agent:ask", "event_data": map[string]any{"question": "what is blocking launch?"}, }, nil) { if err != nil { log.Fatal(err) } if event.Type() == "stream:data" { fmt.Println(event["data_type"], event["payload"]) } if event.Type() == "run:stop" { fmt.Println(event.Run()["result"]) break } } ``` #### **Rust** ```rust use futures_util::StreamExt; use hot_dev::{HotClient, StreamEventExt, SubscribeWithEventOptions}; use serde_json::json; let client = HotClient::builder(std::env::var("HOT_API_KEY").unwrap()).build(); let mut events = client.streams().subscribe_with_event( json!({ "event_type": "team-agent:ask", "event_data": { "question": "what is blocking launch?" }, }), SubscribeWithEventOptions::default(), ); while let Some(event) = events.next().await { let event = event?; if event.event_type() == "stream:data" { println!("{:?} {:?}", event.get("data_type"), event.get("payload")); } if event.event_type() == "run:stop" { println!("{:?}", event.run().and_then(|run| run.get("result"))); break; } } ``` #### **Java** ```java HotClient client = HotClient.builder(System.getenv("HOT_API_KEY")).build(); try (StreamEvents events = client.streams().subscribeWithEvent(Map.of( "event_type", "team-agent:ask", "event_data", Map.of("question", "what is blocking launch?")))) { while (events.hasNext()) { Map event = events.next(); if (StreamEvents.typeOf(event).equals("stream:data")) { System.out.println(event.get("data_type") + " " + event.get("payload")); } if (StreamEvents.typeOf(event).equals("run:stop")) { System.out.println(StreamEvents.runOf(event).get("result")); break; } } } ``` ## Call a Hot Function Every SDK wraps the publish-and-wait flow for `hot:call` events. The helper publishes through the atomic subscribe-with-event endpoint, correlates the terminal run to the published event, and reconnects without publishing twice: #### **JavaScript** ```javascript const result = await hot.events.callHot("::myapp::math/add-nums", [2, 3]); // result === 5 ``` #### **Python** ```python result = hot.events.call_hot("::myapp::math/add-nums", [2, 3]) # result == 5 ``` #### **Go** ```go result, err := client.Events.CallHot(ctx, "::myapp::math/add-nums", []any{2, 3}, nil) // result == float64(5) ``` #### **Rust** ```rust let result = client .events() .call_hot("::myapp::math/add-nums", vec![json!(2), json!(3)], CallOptions::default()) .await?; // result == json!(5) ``` #### **Java** ```java Object result = client.events().callHot("::myapp::math/add-nums", List.of(2, 3)); // result equals 5 ``` ## Wait for a Run If another API response or stored record gives your client a run ID, wait on that run directly. The run subscription sends the latest persisted snapshot first, reconnects after transport interruptions, and closes after a terminal state. A failed or cancelled run raises a structured run error containing the terminal run record. | Language | Wait method | |----------|-------------| | JavaScript / TypeScript | `await hot.runs.wait(runId)` | | Python | `hot.runs.wait(run_id)` or `await async_hot.runs.wait(run_id)` | | Go | `client.Runs.Wait(ctx, runID, nil)` | | Rust | `client.runs().wait(run_id, RunWaitOptions::default()).await` | | Java | `client.runs().waitFor(runId)` | Use the stream subscription when you need live `stream:data` or several related runs. Use the run waiter when one run's durable terminal record is the only result you need. ## Wait for a Background Task When a Hot run starts a task for a client, return the task id immediately and let the SDK wait for its durable result. The task subscription sends the latest persisted snapshot first, including a terminal snapshot when the task completed before the client subscribed. Waiters reconnect automatically and return the completed task record. Failed, cancelled, and timed-out tasks raise a structured task error containing that record. #### **JavaScript** ```javascript const task = await hot.tasks.wait(taskId, { timeoutMs: 300_000 }); console.log(task.result); ``` #### **Python** ```python task = hot.tasks.wait(task_id, timeout=300) print(task["result"]) # AsyncHotClient exposes the same resource: task = await async_hot.tasks.wait(task_id, timeout=300) ``` #### **Go** ```go task, err := client.Tasks.Wait(ctx, taskID, &hot.WaitForTaskOptions{ Timeout: 5 * time.Minute, }) if err != nil { log.Fatal(err) } fmt.Println(task["result"]) ``` #### **Rust** ```rust let task = client .tasks() .wait(task_id, TaskWaitOptions { timeout: Duration::from_secs(300), ..TaskWaitOptions::default() }) .await?; println!("{:?}", task.get("result")); ``` #### **Java** ```java Map task = client.tasks().waitFor( taskId, new TaskWaitOptions().timeout(Duration.ofMinutes(5))); System.out.println(task.get("result")); ``` Use `::hot::task/await` inside Hot only when later Hot code in that same execution depends on the task result. Keeping a run alive just so a client can wait defeats the task's asynchronous lifecycle. ### Coordinate several tasks on one stream The stream that contains the originating run also emits durable `task:update` snapshots for every task on that stream. This keeps the event-handler result fully user-defined: it can return one task ID, two task IDs, or a nested domain object. The client interprets that result, then uses the same stream to follow all relevant tasks: ```typescript let streamId: string | undefined; let taskIds: string[] = []; for await (const event of hot.streams.subscribeWithEvent({ event_type: "report:requested", event_data: { report_id: "report_123" }, })) { if (event.type === "event:published") streamId = event.stream_id; if (event.type === "run:stop") { // `task_ids` is application-defined, not imposed by Hot. const result = event.run?.result as { task_ids?: string[] } | undefined; taskIds = result?.task_ids ?? []; break; } } const pending = new Set(taskIds); for await (const event of hot.streams.subscribe(streamId!)) { if (event.type !== "task:update" || !pending.has(event.task.task_id)) continue; if (["failed", "cancelled", "timed_out"].includes(event.task.status)) { throw new Error(`Task ${event.task.task_id} ${event.task.status}`); } if (event.task.status === "completed") pending.delete(event.task.task_id); if (pending.size === 0) break; } ``` Because the stream sends current persisted task snapshots when the client connects, this remains safe if one of the tasks finishes between the handler's `run:stop` and the second subscription. For a single task, `tasks.wait(taskId)` wraps the same durable lifecycle with reconnection, timeout, and structured failure handling. ## Errors Non-2xx responses surface as structured errors with `status_code`, `code`, `request_id`, and `retry_after`: #### **JavaScript** ```javascript import { HotApiError } from "@hot-dev/sdk"; try { await hot.projects.get("missing-project"); } catch (error) { if (error instanceof HotApiError) { console.log(error.status, error.code, error.requestId, error.retryAfter); } } ``` #### **Python** ```python from hot import HotApiError try: hot.projects.get("missing-project") except HotApiError as error: print(error.status_code, error.code, error.request_id, error.retry_after) ``` #### **Go** ```go _, err := client.Projects.Get(ctx, "missing-project") var apiErr *hot.APIError if errors.As(err, &apiErr) { fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.RequestID, apiErr.RetryAfter) } ``` #### **Rust** ```rust match client.projects().get("missing-project").await { Err(hot_dev::Error::Api(error)) => { println!("{} {:?} {:?} {:?}", error.status_code, error.code, error.request_id, error.retry_after); } other => drop(other), } ``` #### **Java** ```java try { client.projects().get("missing-project"); } catch (HotApiException error) { System.out.println(error.statusCode() + " " + error.code() + " " + error.requestId() + " " + error.retryAfter()); } ``` ## Shared Behavior All five SDKs follow the same conventions: - **Wire-format payloads.** Request and response payloads use the API wire format (`event_type`, `event_data`, `stream_id`). SDK-only options use each language's idiom (`baseUrl`, `base_url`, `BaseURL`). No SDK ever transforms user-owned payloads such as `event_data`. - **Retries.** JSON requests retry automatically (at most twice) when the API responds 429 with a `retry_after`. Streaming and raw requests never retry. - **Streaming reconnection.** `subscribeWithEvent` resubscribes across the API's 5-minute SSE timeout, dedupes replayed `run:start` and terminal events by `run_id`, and ends after the terminal run correlated to the event it published. Unrelated runs on the same stream do not end the iterator. Use the plain `subscribe` when your app expects multiple independent runs on one stream. A plain stream subscription also carries durable `task:update` snapshots for tasks associated with that stream. - **Durable run waiting.** `runs.wait` (or the language-equivalent method) reads `run:update` snapshots and reconnects without missing a run that became terminal before subscription setup. - **Durable task waiting.** `tasks.wait` (or the language-equivalent method) reads `task:update` snapshots, reconnects across transport interruptions, and cannot miss a task that became terminal before subscription setup. - **Identification.** Each SDK sends `User-Agent: hot-sdk-/`. - **Escape hatches.** A `request(...)` method (plus a raw-response variant) covers endpoints that do not yet have a resource helper. Building chat or agent frontends? The JavaScript SDK additionally ships agent, webhook, and BFF proxy helpers as subpath exports (`@hot-dev/sdk/agent`, `/webhook`, `/proxy`); the other SDKs intentionally cover the core API. --- Source: https://hot.dev/docs/mcp # MCP Services The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard that allows AI models and agents to discover and invoke tools. Hot functions can be exposed as MCP tools, making them callable by any MCP-compatible client—such as Claude, Cursor, or custom AI agents. MCP tools are defined using the `mcp` metadata on Hot functions. When you deploy your code, Hot automatically registers these functions as MCP tools, generates JSON schemas from their type signatures, and serves them via a standards-compliant MCP endpoint. ## Defining MCP Tools Add `mcp` metadata to any function to expose it as an MCP tool. The only required field is `service`, which groups related tools together. ### Basic Example ```hot ::myapp::weather ns get-forecast meta {mcp: {service: "weather"}} fn (city: Str, days: Int): Map { ::http/get(`https://api.weather.com/forecast?city=${city}&days=${days}`).body } get-current 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 } ``` This registers two tools under the `weather` service. An MCP client connecting to the `weather` service endpoint will discover both `get-forecast` and `get-current` as available tools. ### Full Example with All Options ```hot ::myapp::geo ns meta {ctx: {"geocode.api.key": {required: true}}} ::http ::hot::http ::ctx ::hot::ctx lookup-address meta { mcp: { service: "geo-tools", name: "lookup-address", title: "Address Lookup", description: "Geocode an address and return coordinates, timezone, and formatted address", annotations: { readOnlyHint: true, openWorldHint: true } } } fn (address: Str, country-code: Str): Map { api-key ::ctx/get("geocode.api.key") ::http/get(`https://api.geocode.com/v1/search?q=${address}&cc=${country-code}&key=${api-key}`).body } ``` Note how `::hot::ctx/get` is used to retrieve an API key stored as a [context variable](/docs/app#context-variables). This keeps secrets out of your source code—configure them per environment in the Hot App. ## Metadata Fields The `mcp` metadata is a map with the following fields: | Field | Required | Description | |-------|----------|-------------| | `service` | Yes | Groups tools into a named service. Clients connect to a specific service endpoint. | | `auth` | No | `"required"` (default) or `"none"`. Controls whether Hot validates credentials before invocation. See [Authentication](#authentication). | | `name` | No | Override the auto-generated tool name. Defaults to `namespace_function_name` with hyphens normalized to underscores (e.g., `::myapp::weather`'s `get-forecast` becomes `myapp_weather_get_forecast`). | | `description` | No | Human-readable description of what the tool does. Helps AI models choose the right tool. | | `title` | No | Display title for the tool. | | `input-schema` | No | Override the auto-generated input JSON Schema. By default, Hot generates this from the function's parameter types. | | `output-schema` | No | Override the auto-generated output JSON Schema. By default, Hot generates this from the function's return type. | | `icons` | No | Tool icons for display in MCP clients. | | `annotations` | No | MCP tool annotations providing behavioral hints to clients. | ### Annotations The `annotations` field follows the MCP specification for tool annotations. These provide hints to clients about the tool's behavior: | Annotation | Type | Description | |------------|------|-------------| | `readOnlyHint` | Bool | Tool does not modify any state | | `destructiveHint` | Bool | Tool may perform destructive operations (delete, overwrite) | | `idempotentHint` | Bool | Calling with same args multiple times has same effect as once | | `openWorldHint` | Bool | Tool interacts with external systems beyond the server | ```hot safe-lookup meta { mcp: { service: "my-service", annotations: { readOnlyHint: true, openWorldHint: false } } } fn (id: Str): Map { ::db/get("records", id) } ``` ## Auto-Generated Schemas Hot automatically generates JSON Schema for your MCP tool's input and output based on the function's type signature. You rarely need to provide schemas manually. ```hot // Hot automatically generates the input schema from these typed parameters search-users meta { mcp: { service: "users", description: "Search users by name and role" } } fn (name: Str, role: Str, active: Bool): Vec { ::db/query("SELECT * FROM users WHERE name LIKE ? AND role = ? AND active = ?", [name, role, active]) } ``` The generated input schema would be: ```json { "type": "object", "properties": { "name": {"type": "string"}, "role": {"type": "string"}, "active": {"type": "boolean"} }, "required": ["name", "role", "active"] } ``` Custom types are also resolved automatically: ```hot SearchParams type { query: Str, page: Int, per-page: Int } search meta {mcp: {service: "search"}} fn (params: SearchParams): Vec { // ... } ``` For MCP 2026-07-28, Hot validates each advertised input and output schema as JSON Schema. Tool results with an `output-schema` are validated before they are returned. Unlike older MCP clients, modern clients can receive any JSON value in `structuredContent`; Hot automatically wraps non-object results as `{"result": ...}` only when serving a legacy client. ### Routing Parameters with `x-mcp-header` The 2026-07-28 Streamable HTTP transport can mirror selected tool arguments into headers for routing by a load balancer, gateway, or WAF. Put `x-mcp-header` on a statically reachable string, integer, or boolean property in a custom input schema: ```hot route-query meta { mcp: { service: "analytics", input-schema: { type: "object", properties: { tenant: { type: "string", "x-mcp-header": "Tenant" }, query: {type: "string"} }, required: ["tenant", "query"] } } } fn (tenant: Str, query: Str): Map { // A modern client sends Mcp-Param-Tenant with the same value. run-query(tenant, query) } ``` Hot checks the mirrored header against the JSON body before invoking the tool. Header names must be unique HTTP tokens. Values unsafe for an HTTP header are encoded using the MCP `=?base64?...?=` sentinel. Do not mark credentials, tokens, or PII for header mirroring. ## Services Services are the organizational unit for MCP tools. Each service gets its own MCP endpoint and groups related tools together. ### Naming Conventions Choose meaningful service names that describe the domain: ```hot // Good: descriptive service names meta {mcp: {service: "weather"}} meta {mcp: {service: "user-management"}} meta {mcp: {service: "data-analytics"}} // Avoid: generic or overly broad names meta {mcp: {service: "tools"}} meta {mcp: {service: "api"}} ``` ### Multiple Services A single project can expose tools across multiple services. Functions in different namespaces can belong to the same service, and functions in the same namespace can belong to different services: ```hot ::myapp::users ns // Both in the "admin" service list-users meta { mcp: { service: "admin", description: "List all users" } } fn (): Vec { ::db/query("SELECT * FROM users") } create-user meta { mcp: { service: "admin", description: "Create a new user" } } fn (name: Str, email: Str): Map { ::db/insert("users", {name: name, email: email}) } ``` ```hot ::myapp::reports ns // In a separate "reports" service generate-report meta { mcp: { service: "reports", description: "Generate a usage report" } } fn (start-date: Str, end-date: Str): Map { ... } ``` ## MCP Endpoint Once deployed, your MCP tools are available at: ``` https://api.hot.dev/mcp/{org-slug}/{env-name}/{service} ``` For local development with `hot dev`, the default org slug is `local` and the default environment is `development`: ``` http://localhost:4681/mcp/local/development/{service} ``` ### Custom Domain URLs If you have a [custom domain](/docs/domains) configured for your environment, you can use shorter URLs that omit the org and env: ``` https://your-domain.com/mcp/{service} ``` The organization and environment are resolved from the domain automatically. Both the standard URL and the custom domain URL work identically—the custom domain version is just shorter and branded. The Hot App dashboard shows a domain selector when custom domains are available, letting you switch between the default URL and your custom domain URLs. ### Authentication By default, MCP tools require authentication via the `Authorization` header: ``` Authorization: Bearer YOUR_TOKEN ``` Hot supports multiple auth modes per tool, controlled by the `auth` field in the `mcp` metadata. The default is `"required"`. MCP service URLs are public identifiers, not secrets. Clients can call `tools/list` on a reachable service endpoint to discover tool names, descriptions, and input schemas. Tool execution still follows each tool's `auth` setting, so keep the default `"required"` unless you intentionally want a public tool. #### API Keys The standard authentication method. API keys are environment-scoped and can be restricted to specific MCP services via [permissions](#api-key-permissions). ```hot // Default — auth: "required" (API key, service key, or session) get-forecast meta {mcp: {service: "weather"}} fn (city: Str): Map { ::http/get(`https://api.weather.com/forecast?city=${city}`).body } ``` #### Service Keys [Service keys](/docs/authentication#service-keys) are long-lived, permission-scoped credentials for your customers. Attach metadata (e.g., customer ID, plan) to a service key, and it's available at runtime via `hot.request`: ```hot get-usage meta {mcp: {service: "billing", description: "Get usage for the calling customer"}} fn (): Map { req ::hot::ctx/get("hot.request") customer-id req.auth.service-key.meta.customer_id fetch-usage-for(customer-id) } ``` See [Caller Identity](#caller-identity-hotrequest) for the full `hot.request` structure. #### Public Tools Set `auth: "none"` to make a tool publicly accessible — no credentials required. ```hot hash-text meta {mcp: {service: "utils", auth: "none"}} fn (text: Str): Str { ::hot::hash/sha256(text) } ``` #### Pass-Through Auth `auth: "none"` also enables pass-through auth patterns, where the MCP endpoint is open but your function extracts client-provided credentials from HTTP headers and relays them to downstream APIs. The caller's headers are available via `hot.request.headers`: ```hot chat meta { mcp: { service: "ai-proxy", auth: "none", description: "Chat with an LLM. Requires x-api-key header with your OpenAI key." } } fn (model: Str, message: Str): Map { req ::hot::ctx/get("hot.request") api-key get(req.headers, "x-api-key") if(is-null(api-key), fail("x-api-key header required")) ::http/post("https://api.openai.com/v1/chat/completions", { headers: {"Authorization": `Bearer ${api-key}`}, body: {model: model, messages: [{role: "user", content: message}]} }).body } ``` This is useful for BYOK (bring your own key) patterns where your customers provide their own API keys for third-party services. #### Auth Modes Summary | `auth` value | Default | Behavior | |---|---|---| | `"required"` | Yes | Hot validates credentials (API key, service key, or session). `hot.request.auth` contains caller identity. | | `"none"` | No | No Hot credential check. Use for public tools or pass-through auth via `hot.request.headers`. | #### Secret Headers Certain HTTP headers are automatically masked in run logs to prevent credential leakage: `authorization`, `cookie`, `proxy-authorization`, and `set-cookie`. Values from the `hot.request.auth` subtree are always masked as well. If your tool receives custom credentials via headers (e.g., `x-api-key`), declare them in the top-level `secret-headers` metadata so they are also masked: ```hot list-invoices meta { mcp: {service: "billing", auth: "none"}, secret-headers: ["x-api-key", "x-customer-secret"] } fn (status: Str?): Vec { req ::ctx/get("hot.request") api-key req.headers.x-api-key // api-key value is masked in run logs ... } ``` Non-sensitive headers (like `content-type`, `user-agent`) and other `hot.request` fields (`method`, `url`, `query`, `ip`) are **not** masked, so they remain visible in run logs for debugging. ### Protocol The same Hot endpoint supports the new stateless protocol and existing MCP clients. A 2026-07-28 request is selected by its required request metadata and routing headers; legacy requests continue through their initialize-based codec. | Protocol era | Endpoint(s) | Notes | |--------------|-------------|-------| | Stateless Streamable HTTP (2026-07-28) | `POST /mcp/{org}/{env}/{service}` | Begins with `server/discover`; every request carries version, client metadata, capabilities, and routing headers. No MCP session is created. | | Legacy Streamable HTTP (2025-03-26 through 2025-11-25) | `POST /mcp/{org}/{env}/{service}` | Uses `initialize` and `notifications/initialized`. Existing clients continue to work on the same URL. | | HTTP+SSE (2024-11-05, deprecated) | `GET /mcp/{org}/{env}/{service}` + `POST /mcp/{org}/{env}/{service}/messages?sessionId=...` | `GET` opens SSE and returns an `endpoint` event; `POST` sends JSON-RPC messages; responses arrive on the SSE stream. | Supported methods: | Method | Description | |--------|-------------| | `server/discover` | Discover 2026-07-28 versions and server capabilities | | `initialize` | Initialize a legacy MCP session | | `ping` | Legacy health/liveness check | | `notifications/initialized` | Legacy initialization acknowledgement | | `tools/list` | List all available tools for this service | | `tools/call` | Execute a tool with arguments. Response may be JSON or SSE (`event: message` containing JSON-RPC payloads). | Modern requests must include `MCP-Protocol-Version` and `Mcp-Method`. `tools/call` also includes `Mcp-Name`, plus any tool parameter headers declared with `x-mcp-header`. Every successful modern result has a `resultType`; discovery and tool lists also carry cache metadata. Hot-hosted MCP services currently advertise `tools.listChanged: false`, so they do not open a `subscriptions/listen` stream. The `hot.dev/mcp` client package supports the method for external servers that advertise tool, prompt, or resource notification capabilities. Browser-origin requests are accepted only when the `Origin` is the endpoint's own origin or appears in `hot.mcp.allowed-origins`: ```hot hot.mcp.allowed-origins [ "https://agent.example.com", "http://localhost:3000" ] ``` ### Timeouts - `mcp.timeout` controls Streamable HTTP `tools/call` execution timeout (default: `60` seconds). - `mcp.http-sse.session-timeout` controls HTTP+SSE transport session lifetime for `GET /mcp/{org}/{env}/{service}` (default: `300` seconds). ### Example: Modern Discovery with curl For local development, replace the base URL with `http://localhost:4681/mcp/local/development/{service}`. ```bash # Stateless 2026-07-28 discovery curl -X POST https://api.hot.dev/mcp/my-org/production/weather \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } }' ``` The Hot `hot.dev/mcp` package builds these headers and metadata automatically. For example: ```hot ::mcp ::mcp::client ::types ::mcp::types session ::mcp/connect( "https://api.hot.dev/mcp/my-org/production/weather", ::types/ClientInfo({name: "my-client", version: "1.0"}), null, ::types/ConnectionOptions({ headers: {"Authorization": `Bearer ${api-key}`} }) ) ``` To verify legacy compatibility manually, use the same endpoint with the initialize handshake: ```bash curl -X POST https://api.hot.dev/mcp/my-org/production/weather \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "my-client", "version": "1.0"} } }' ``` ### Example: Streaming `tools/call` Response (Streamable HTTP) For long-running tool calls, use `-N` so curl prints SSE chunks as they arrive: ```bash curl -N -X POST https://api.hot.dev/mcp/my-org/production/weather \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: myapp_weather_get_forecast" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "myapp_weather_get_forecast", "arguments": {"city": "San Francisco", "days": 5}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } }' # Example output # event: message # data: {"jsonrpc":"2.0","method":"notifications/message", ...} # # event: message # data: {"jsonrpc":"2.0","id":4,"result":{...}} ``` ### Example: Deprecated HTTP+SSE Transport ```bash # 1) Open SSE stream and capture the endpoint event curl -N https://api.hot.dev/mcp/my-org/production/weather \ -H "Authorization: Bearer $HOT_API_KEY" # First SSE event: # event: endpoint # data: /mcp/my-org/production/weather/messages?sessionId= # 2) POST JSON-RPC to the messages endpoint from the endpoint event curl -X POST "https://api.hot.dev/mcp/my-org/production/weather/messages?sessionId=" \ -H "Authorization: Bearer $HOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' # 3) Read JSON-RPC responses from the existing SSE stream ``` ### Connecting AI Clients Most MCP-compatible AI clients can connect directly to your Hot MCP endpoint. Configure them with: - **URL**: `https://api.hot.dev/mcp/{org}/{env}/{service}` (or `https://your-domain.com/mcp/{service}` with a [custom domain](#custom-domain-urls)) - **Transport**: Streamable HTTP (preferred) or HTTP+SSE (deprecated compatibility) - **Authentication**: Bearer token (API key, service key, or session). Not required for tools with `auth: "none"`. ## Caller Identity (`hot.request`) When an MCP tool is invoked, Hot automatically populates the `hot.request` context variable with an `::hot::http/HttpRequest` containing HTTP request details and caller identity. Access it via `::hot::ctx/get("hot.request")`. This is the same `HttpRequest` type used by [webhooks](/docs/webhooks#httprequest-and-httpresponse). For MCP, the body fields (`body`, `body-raw`, `body-bytes`) and `original-url` are `null` — the tool arguments come from the MCP protocol, not the HTTP body, and URL reconstruction is a webhook-delivery concern. ### Structure ```hot ::ctx ::hot::ctx req ::ctx/get("hot.request") // HTTP context — always present req.method // "POST" req.url // "/mcp/my-org/production/weather" req.headers // Map — all HTTP headers (lowercased keys) req.query // Map — query string parameters req.ip // client IP address // 2026-07-28 MCP context — present for modern tool calls req.mcp.protocol-version // "2026-07-28" req.mcp.client-capabilities // capabilities from request _meta req.mcp.input-responses // responses supplied on an MRTR retry, or null req.mcp.request-state // opaque state echoed on an MRTR retry, or null // Auth context — present when auth: "required" (default) req.auth.type // "api-key" | "service-key" | "session" req.auth.service-key.id // service key UUID (when type = "service-key") req.auth.service-key.name // service key name (when type = "service-key") req.auth.service-key.meta // service key metadata (when type = "service-key") // When auth: "none" req.auth // null ``` ## Multi Round-Trip Tool Input A modern tool can return `resultType: "input_required"` when it needs elicitation, sampling, or roots data from a capability the client advertised. The client retries the original call with a new JSON-RPC ID. Hot exposes that retry data through `hot.request.mcp`. ```hot publish-report meta {mcp: {service: "reports"}} fn (report-id: Str): Map { req ::hot::ctx/get("hot.request") response get(req.mcp.input-responses, "confirm", null) if(is-null(response), { "resultType": "input_required", "inputRequests": { confirm: { method: "elicitation/create", params: { mode: "form", message: "Publish this report?", requestedSchema: { type: "object", properties: {confirmed: {type: "boolean"}}, required: ["confirmed"] } } } }, // Treat requestState as attacker-controlled input. Integrity-protect it // whenever it affects authorization or resource access. "requestState": report-id }, { publish(report-id, response.content.confirmed) }) } ``` Hot rejects input requests for capabilities the caller did not advertise. Applications remain responsible for integrity protection, expiry, principal binding, and replay rules for security-sensitive `requestState` values. ### Using Headers HTTP headers are available on all requests regardless of auth mode. Header keys are lowercased: ```hot get-forecast meta {mcp: {service: "weather"}} fn (city: Str): Map { req ::hot::ctx/get("hot.request") region or(get(req.headers, "x-region"), "us-east-1") fetch-forecast(city, region) } ``` For `auth: "none"` tools, headers are the mechanism for pass-through auth: ```hot proxy-api meta {mcp: {service: "proxy", auth: "none"}} fn (endpoint: Str): Map { req ::hot::ctx/get("hot.request") token get(req.headers, "authorization") if(is-null(token), fail("Authorization header required")) ::http/get(endpoint, {headers: {"Authorization": token}}).body } ``` ### Using Metadata for Customer Context Service key metadata is the recommended way to pass customer context into your Hot functions. When you create a service key for a customer and attach metadata (e.g., `{"customer_id": "acme-123", "plan": "enterprise"}`), that metadata is automatically decrypted and available at runtime: ```hot ::myapp::billing ns ::ctx ::hot::ctx get-usage meta {mcp: {service: "billing", description: "Get usage for the calling customer"}} fn (): Map { req ::ctx/get("hot.request") customer-id req.auth.service-key.meta.customer_id fetch-usage-for(customer-id) } ``` This lets you build multi-tenant MCP services where each customer's service key carries their identity, and your functions can use it to scope data access, enforce limits, or customize behavior — without requiring the customer to pass their own ID as a parameter. ### Security Sensitive values in `hot.request` are automatically masked in run logs. Specifically: - The entire `auth` subtree (credential type, service key metadata, etc.) - Values of sensitive HTTP headers: `authorization`, `cookie`, `proxy-authorization`, `set-cookie` - Values of any headers declared in `secret-headers` metadata Non-sensitive fields like `method`, `url`, `query`, `ip`, and non-sensitive headers remain visible in run logs for debugging. ## API Key Permissions API keys can be restricted to only allow MCP access, and further restricted to specific services: | Permission | Format | Access | |------------|--------|--------| | Full Access | `{"*:*": ["*"]}` | Unrestricted access to all API endpoints including MCP | | MCP (all services) | `{"mcp:*": ["execute"]}` | MCP tool invocation for all services | | MCP (specific service) | `{"mcp:weather": ["execute"]}` | MCP tool invocation for a specific service only | For example, an API key with permission `{"mcp:weather": ["execute"]}` can only invoke tools in the `weather` service. It cannot access other services or any non-MCP API endpoints. Permissions are configured when creating or editing API keys in the Hot App. See [Hot App > API Keys](/docs/app#api-keys) for details. ## Lifecycle MCP tools are driven by metadata in your source code: 1. **Define**: Add `mcp` metadata to functions in your Hot code 2. **Deploy**: Run `hot deploy` (or `hot dev` for local development) 3. **Discover**: MCP clients connect and call `tools/list` to discover available tools 4. **Invoke**: Clients call `tools/call` to execute tools; Hot runs the function and returns the result When you redeploy, the tool registry updates automatically. If a function's `mcp` metadata is removed, the tool is unregistered. If it's added back, the tool reappears—API key permissions that reference the service are preserved across these changes. ## Retries The `retry` metadata does **not** apply to MCP tool invocations. MCP uses a synchronous request/response model—the client sends a `tools/call` request and waits for the result. If the function fails, the error is returned immediately to the MCP client. The client can then decide whether to retry. This differs from event handlers and scheduled functions, which run asynchronously and benefit from server-side retries. If you have a function that serves as both an MCP tool and an event handler, the `retry` configuration will apply to event-triggered runs but not to MCP-triggered runs. ```hot // retry applies to event handler runs, not MCP tool calls process-data meta { on-event: "data:received", mcp: {service: "data"}, retry: 3 } fn (data: Map): Map { transform-and-store(data) } ``` ## Best Practices **Write clear descriptions.** AI models rely on tool descriptions to decide which tool to use. Be specific about what the tool does, what it returns, and any side effects. ```hot // Good: specific and informative meta { mcp: { service: "crm", description: "Search contacts by name, email, or company. Returns up to 50 matching contacts sorted by relevance." } } // Avoid: vague meta { mcp: { service: "crm", description: "Search contacts" } } ``` **Use typed parameters.** Hot auto-generates JSON Schema from your function signatures. Well-typed parameters produce better schemas, which help AI models provide correct arguments. ```hot // Good: typed parameters with clear names fn (customer-id: Str, start-date: Str, end-date: Str, include-refunds: Bool): Vec { ... } // Less helpful: untyped fn (params: Map): Map { ... } ``` **Group related tools into a service.** Keep services focused on a single domain. This makes it easy to grant targeted API key permissions and helps clients discover related tools. **Use annotations for safety hints.** Mark read-only tools as `readOnlyHint: true` and destructive tools as `destructiveHint: true` so AI clients can make informed decisions about tool usage. --- Source: https://hot.dev/docs/webhooks # Webhooks Webhooks allow external services to send HTTP requests to your Hot functions. When a service like Slack, Stripe, or GitHub needs to notify your application of an event, it sends an HTTP request to a webhook URL. Hot receives the request, runs your function, and returns the response. Webhook endpoints are defined using the `webhook` metadata on Hot functions. When you deploy your code, Hot automatically registers these functions as webhook endpoints and serves them via public HTTP routes. ## Defining Webhook Endpoints Add `webhook` metadata to any function to expose it as a webhook endpoint. The required fields are `service` (a logical grouping) and `path` (the URL path within that service). ### Basic Example ```hot ::myapp::slack ns on-slack-event meta {webhook: {service: "slack", path: "/events"}} fn (request: HttpRequest): HttpResponse { // Process the incoming Slack event HttpResponse({status: 200, body: {ok: true}}) } ``` This registers a `POST /events` endpoint under the `slack` service. External services send HTTP requests to the webhook URL, and Hot calls your function with an `HttpRequest` (from `::hot::http`). ### Full Example with All Options ```hot ::myapp::payments ns ::stripe-wh ::stripe::webhooks stripe-payment meta { webhook: { service: "stripe", path: "/payment", method: "POST", name: "stripe_payment_handler", description: "Handle Stripe payment webhook events", auth: "required" } } fn (request: HttpRequest): HttpResponse { // Verify the request came from Stripe before trusting it // (see "Provider Signature Verification" below) if(not(::stripe-wh/verify-request(request)), HttpResponse({status: 401, body: {error: "Invalid signature"}}), { event from-json(request.body-raw) process-payment(event) HttpResponse({status: 200, body: {received: true}}) }) } ``` Webhook handlers can be grouped under an [agent](/docs/agents) by adding `agent: TypeName` to the metadata. This enables per-agent run tracking, health metrics, and observability in the Hot App. ## Metadata Fields The `webhook` metadata is a map with the following fields: | Field | Required | Description | |-------|----------|-------------| | `service` | Yes | Groups endpoints into a named service. Part of the webhook URL. Must be URL-safe (alphanumeric, hyphens, underscores, dots). | | `path` | Yes | The URL path for this endpoint within the service (e.g., `/events`, `/payment`). | | `method` | No | HTTP method to match. Defaults to `POST`. Can be `GET`, `PUT`, `PATCH`, `DELETE`, or `POST`. | | `name` | No | Override the auto-generated endpoint name. Defaults to `namespace_function_name`. | | `description` | No | Human-readable description of what the endpoint does. | | `auth` | No | Authentication mode: `"none"` (default, public) or `"required"` (requires Bearer token — API key, service key, or session). | ## HttpRequest and HttpResponse Webhook handlers use the standard `HttpRequest` and `HttpResponse` types from `::hot::http`. ### HttpRequest Your function receives an `HttpRequest` with the full details of the incoming HTTP request: ```hot HttpRequest type { method: Str, // HTTP method (GET, POST, etc.) url: Str, // Request URL path (internal, token-free — safe to log) original-url: Str?, // The URL as the caller requested it, pre-rewrite headers: Map?, // HTTP headers (lowercase keys) query: Map?, // Query string parameters body: Any?, // Parsed body (JSON-decoded if applicable) body-raw: Str?, // Raw request body as a string body-bytes: Bytes?, // Verbatim body bytes (only when not valid UTF-8) ip: Str?, // Client IP address (from proxy headers) auth: Map? // Caller identity (when auth is "required") } ``` When Hot delivers a webhook request, all common fields are populated. The `ip` field is extracted from `x-forwarded-for` or `x-real-ip` proxy headers. The `auth` field is only present when the endpoint has `auth: "required"` and the caller authenticates successfully — see [Caller Identity](#caller-identity-hotrequest) for the full structure. Two fields exist specifically for signature verification. `original-url` is the full URL the provider actually called — scheme, host, path (webhook token included), and query string intact — which is what providers that sign their request URL (Twilio, HubSpot) hash; it contains the webhook token, so don't log it. `body-bytes` carries the verbatim body bytes only when the body is not valid UTF-8 (in that case `body-raw` is a lossy conversion); use `::hot::http/raw-body(request)` to get the right one without checking. This is the same `HttpRequest` type used by [MCP tools](/docs/mcp#caller-identity-hotrequest) (via `hot.request`), providing a unified request representation across both systems. ### HttpResponse Return an `HttpResponse` to control the HTTP response sent back to the caller: ```hot HttpResponse type { status: Int, // HTTP status code (200, 201, 404, etc.) headers: Map?, // Response headers (optional) body: Any? // Response body (will be JSON-encoded, optional) } ``` Only `status` is required. Omit `headers` and `body` when not needed (e.g., a `204 No Content` response). If your function returns a plain value (not an `HttpResponse`), Hot wraps it as a `200 OK` JSON response automatically. ```hot // These are equivalent: fn (request: HttpRequest): HttpResponse { HttpResponse({status: 200, body: {ok: true}}) } fn (request: HttpRequest): Map { {ok: true} // Automatically becomes 200 JSON response } ``` ## Webhook URL Once deployed, your webhook endpoints are available at: ``` https://api.hot.dev/webhook/{org-slug}/{env-name}/{service}/{path}/{token} ``` For local development with `hot dev`: ``` http://localhost:4681/webhook/local/development/{service}/{path}/{token} ``` The final `{token}` segment is a per-endpoint secret generated by Hot. It makes the URL unguessable, so spoofers can't hit your endpoint by knowing only the service and path. Copy the complete URL — including the token — from the **Webhooks** view in the Hot App; requests with a missing or wrong token are rejected. ### Examples | Metadata | URL | |----------|-----| | `service: "slack", path: "/events"` | `https://api.hot.dev/webhook/my-org/production/slack/events/{token}` | | `service: "stripe", path: "/payment"` | `https://api.hot.dev/webhook/my-org/production/stripe/payment/{token}` | | `service: "github", path: "/push"` | `https://api.hot.dev/webhook/my-org/staging/github/push/{token}` | The URL includes both the organization slug and environment name, so you can have separate webhook endpoints for development, staging, and production. ### Custom Domain URLs If you have a [custom domain](/docs/domains) configured for your environment, you can use shorter URLs that omit the org and env: ``` https://your-domain.com/webhook/{service}/{path}/{token} ``` The organization and environment are resolved from the domain automatically. Both the standard URL and the custom domain URL work identically—the custom domain version is just shorter and branded. | Metadata | Default URL | Custom Domain URL | |----------|------------|-------------------| | `service: "slack", path: "/events"` | `https://api.hot.dev/webhook/my-org/production/slack/events/{token}` | `https://hooks.acme.com/webhook/slack/events/{token}` | | `service: "stripe", path: "/payment"` | `https://api.hot.dev/webhook/my-org/production/stripe/payment/{token}` | `https://hooks.acme.com/webhook/stripe/payment/{token}` | The Hot App dashboard shows a domain selector when custom domains are available, letting you switch between the default URL and your custom domain URLs. ## Authentication Webhook endpoints are **public by default**—no API key is required. This is necessary because external services (Slack, Stripe, GitHub, etc.) cannot provide your API key when sending webhook requests. ### Optional API Key Authentication For webhooks where you control the sender, you can require authentication: ```hot internal-hook meta { webhook: { service: "internal", path: "/sync", auth: "required" } } fn (request: HttpRequest): Map { // Only accessible with a valid credential sync-data(request.body) } ``` When `auth` is set to `"required"`, the caller must include an `Authorization: Bearer ` header. The token can be an API key, service key, or session token. The credential must have a `webhook` permission (e.g., `{"webhook:*": ["execute"]}` or `{"webhook:internal/*": ["execute"]}`). Authenticated webhook handlers receive the caller's identity in the `auth` field of the `HttpRequest` argument. The same data is also available via `::hot::ctx/get("hot.request")` for consistency with MCP tools. ### Caller Identity (`hot.request`) Every webhook invocation — authenticated or not — populates the `hot.request` context variable with the same `HttpRequest` that your function receives as its argument. Access it via `::hot::ctx/get("hot.request")`. When the endpoint requires authentication, `hot.request.auth` (and `request.auth`) contains the caller's identity: ```hot internal-sync meta {webhook: {service: "internal", path: "/sync", auth: "required"}} fn (request: HttpRequest): Map { request.auth.type // "api-key" | "service-key" | "session" request.auth.service-key.meta // service key metadata (if service key) sync-data(request.body) } ``` ### Secret Headers Certain HTTP headers are automatically masked in run logs to prevent credential leakage: `authorization`, `cookie`, `proxy-authorization`, and `set-cookie`. Values from the `auth` subtree are always masked as well. If your webhook receives custom credentials via headers, declare them in the top-level `secret-headers` metadata so they are also masked: ```hot stripe-payment meta { webhook: {service: "stripe", path: "/payment"}, secret-headers: ["stripe-signature"] } fn (request: HttpRequest): HttpResponse { sig get(request.headers, "stripe-signature", "") // sig value is masked in run logs process-payment(request.body) HttpResponse({status: 200, body: {received: true}}) } ``` Non-sensitive headers (like `content-type`, `user-agent`) and other request fields (`method`, `url`, `query`, `ip`) are **not** masked, so they remain visible in run logs for debugging. ### Provider Signature Verification For external providers, authenticate requests by verifying their cryptographic signature in your Hot code. Most providers (Slack, Stripe, GitHub, etc.) sign webhook payloads using HMAC-SHA256 or similar. ```hot ::slack ::slack::webhooks ::ctx ::hot::ctx on-slack-event meta {webhook: {service: "slack", path: "/events"}} fn (request: HttpRequest): HttpResponse { // Verify the request is from Slack signing-secret ::ctx/get("slack.signing.secret") if(not(::slack/verify-request(request, signing-secret)), { HttpResponse({status: 401, body: {error: "Invalid signature"}}) }, { // Process the verified event event from-json(request.body-raw) handle-slack-event(event) HttpResponse({status: 200, body: {ok: true}}) }) } ``` Hot's provider packages ship `verify-request` functions with the correct recipe for each provider: Slack, Stripe, GitHub, Shopify, WhatsApp, and Discord verify over the payload (and timestamp, where the provider signs one), while Twilio and HubSpot — which sign the URL they call — verify against `request.original-url` automatically. Each takes the secret from a context variable in its 1-arity form or explicitly in its 2-arity form, and the timestamp-checking verifiers accept a replay-window tolerance (0 disables it). For providers without a package, write a verifier with the fail-closed helpers in `::hot::hmac` (`hmac-verify-hex`, `hmac-verify-base64` — constant-time, false on malformed input) and `::hot::time/within-seconds-of-now` for replay windows, hashing `::hot::http/raw-body(request)`. ## API Key Permissions API keys can be restricted to only allow webhook access, and further restricted to specific services: | Permission | Format | Access | |------------|--------|--------| | Full Access | `{"*:*": ["*"]}` | Unrestricted access to all API endpoints including webhooks | | Webhooks (all services) | `{"webhook:*": ["execute"]}` | Webhook endpoint access for all services | | Webhooks (specific service) | `{"webhook:internal": ["execute"]}` | Webhook endpoint access for a specific service only | For example, an API key with permission `{"webhook:internal": ["execute"]}` can only call webhook endpoints in the `internal` service. It cannot access other services or any non-webhook API endpoints. Permissions are configured when creating or editing API keys in the Hot App. See [Hot App > API Keys](/docs/app#api-keys) for details. ## Synchronous Webhook Handling Webhook handlers execute **synchronously**. When a request arrives, Hot calls your function and waits for it to return before sending the HTTP response. This is important for services like Slack that require a response within 3 seconds. The handler invocation is one platform run; ordinary function calls made by the handler remain inside that run's trace. See the [Platform Execution Model](/docs/platform/execution-model) for the distinction between calls, runs, and child events. For long-running work, acknowledge the webhook immediately and process asynchronously using `send()`: ```hot on-slack-event meta {webhook: {service: "slack", path: "/events"}} fn (request: HttpRequest): HttpResponse { // Acknowledge immediately event from-json(request.body-raw) send("slack:event:received", event.data) // Return 200 right away (processing happens in event handler) HttpResponse({status: 200, body: {ok: true}}) } // Separate event handler does the heavy lifting process-slack-event meta {on-event: "slack:event:received", retry: 3} fn (event) { // This runs asynchronously with retries do-expensive-work(event.data) } ``` ## Lifecycle Webhook endpoints are driven by metadata in your source code: 1. **Define**: Add `webhook` metadata to functions in your Hot code 2. **Deploy**: Run `hot deploy` (or `hot dev` for local development) 3. **Configure**: Give the webhook URL to the external service 4. **Receive**: External services send HTTP requests; Hot calls your function and returns the response When you redeploy, the endpoint registry updates automatically. If a function's `webhook` metadata is removed, the endpoint is unregistered. ## Retries The `retry` metadata does **not** apply to webhook invocations. Webhooks use a synchronous request/response model—the caller sends a request and waits for the result. If the function fails, the error is returned immediately as a 500 response. The calling service can then decide whether to retry. Use the `send()` pattern shown above to defer work to event handlers that support server-side retries. ## Best Practices **Respond quickly.** Many webhook providers have strict timeouts (Slack requires a response within 3 seconds). Acknowledge the webhook immediately and defer heavy processing to event handlers. **Verify signatures.** Always verify webhook signatures for external providers. Never trust incoming requests without verification—webhook URLs are public and can receive spoofed requests. **Use the raw body for signature verification.** Signature verification requires the exact bytes the sender signed — never `request.body` (which is parsed). Use `::hot::http/raw-body(request)`: it returns `body-raw` (the original string) for normal payloads and `body-bytes` for non-UTF-8 payloads, where `body-raw` alone would be lossy. **Return appropriate status codes.** Return `200` for success, `401` for authentication failures, and `400` for bad requests. Many providers will retry on `5xx` errors, so only return 500 for genuine failures. ```hot // Good: specific status codes HttpResponse({status: 200, body: {ok: true}}) HttpResponse({status: 401, body: {error: "Invalid signature"}}) HttpResponse({status: 400, body: {error: "Missing event type"}}) ``` **Group related endpoints by service.** Use meaningful service names that match the provider or domain: ```hot meta {webhook: {service: "slack", path: "/events"}} meta {webhook: {service: "slack", path: "/commands"}} meta {webhook: {service: "stripe", path: "/payment"}} meta {webhook: {service: "github", path: "/push"}} ``` --- Source: https://hot.dev/docs/domains # Custom Domains **Custom Domains** let you map your own domain names (e.g., `mcp.example.com`, `webhook.example.com`) to your Hot Dev environment. Instead of your customers connecting to `api.hot.dev`, they connect to your branded domain. > **Pro plan required.** Custom domains are available on Pro and Scale plans. If you're on the Starter plan, the Domains page shows an upgrade prompt. Plan limits: Pro allows up to 5 custom domains per organization, Scale up to 25, and Self-Host is unlimited. Multiple domains can be mapped to the same environment - for example, `mcp.example.com` and `webhook.example.com` can both route to the full API surface. ## Adding a Domain Click **Add Domain** in the [Hot App](/docs/app) and enter your domain name. Hot Dev will request a TLS certificate from the configured domain provider and begin provisioning. The domain detail page guides you through three steps: 1. **Request Certificate** - Hot Dev requests a TLS certificate automatically. DNS validation records appear within a few seconds. 2. **Validate & Issue** - Add the validation CNAME record shown on the detail page to prove domain ownership. Once DNS propagates, the certificate is issued. 3. **Domain CNAME** - Once the certificate is validated, a routing target is created automatically. Add a CNAME pointing your domain to the target shown in the app. This record appears once provisioning completes. The detail page updates automatically - you can leave it open and watch each step progress without refreshing. ## Verification & Status Click **Check Status** on the domain detail page to trigger an immediate recheck. When a routing target exists, Check Status also performs a live DNS lookup to verify your domain's CNAME record is pointing to the correct target. You don't have to keep clicking Check Status - pending domains are also checked automatically in the background. Once DNS propagation completes, your domain will be validated and provisioned without any manual action. | Status | Meaning | |--------|---------| | Pending Validation | Certificate validation CNAME not yet detected | | Validated | Certificate issued, routing target provisioning in progress | | Provisioning | Routing target being created or deployed | | Active | HTTPS is active - domain CNAME is configured and routing traffic | | Deleting | Domain removal in progress - provider resources are being cleaned up | ## Using Custom Domains Once a domain is active, use it anywhere you'd use `api.hot.dev`: - **MCP endpoints** - Point AI agents to `mcp.example.com` instead of `api.hot.dev` - **Webhook URLs** - Give external services your branded webhook URL - **API calls** - Use your domain for all Hot API requests The domain routes to the same Hot API surface, so all existing [API keys](/docs/authentication#api-keys), [MCP services](/docs/mcp), and [webhooks](/docs/webhooks) work automatically. ## Removing a Domain Click **Remove Domain** on the domain detail page to delete a custom domain. This soft-deletes the domain and queues cleanup of the associated provider resources. While cleanup is in progress, the domain shows a **Deleting** status. You cannot re-add the same domain name until cleanup completes - attempting to do so will show a message asking you to wait. When removing a domain, remember to also delete the DNS CNAME records you created for it (both the validation CNAME and the domain CNAME). ## API Access Custom domains can also be managed programmatically via the [Custom Domains API](/docs/api#custom-domains). --- Source: https://hot.dev/docs/authentication # Authentication The [Hot API](/docs/api) supports three credential types for authenticating requests. All are passed in the `Authorization` header as Bearer tokens and scoped to an environment. | Credential | Token Format | Lifetime | Purpose | |-----------|-------------|----------|---------| | **API Key** | `hot__` | Long-lived | Full environment access for you and your team | | **Service Key** | `_` | Long-lived (optional expiry) | Permission-scoped access for your customers and integrations | | **Session** | `s__` | Short-lived (1h default, 24h max) | Ephemeral, permission-scoped access for browser clients | ```bash curl -H "Authorization: Bearer $TOKEN" https://api.hot.dev/v1/projects ``` ## API Keys **API Keys** are your primary credentials for accessing the Hot API. Create and manage them from the [Hot App](/docs/app) dashboard. - Create keys with descriptive names - Enable or disable keys without deleting them - Keys are scoped to environments ### Access Levels When creating or editing an API key, you choose between two access levels: - **Full Access** — Unrestricted access to all API endpoints (the default) - **Restricted** — Limit the key to specific capabilities using the [permissions model](#permissions-model) ## Service Keys **Service Keys** are long-lived, permission-scoped credentials designed for your customers and external integrations. If you're building a platform on Hot Dev and need to give your customers direct API access (e.g., to [MCP tools](/docs/mcp) or streams), service keys let you issue narrowly scoped tokens under your API key with granular permissions. Service keys can carry **customer metadata** — arbitrary JSON that is encrypted at rest and automatically available to your Hot functions at runtime. This lets you identify callers and pass customer context (e.g., account ID, plan tier) without requiring extra parameters in every request. ### Creating Service Keys From the [Hot App](/docs/app), click **New Service Key**. You'll specify: - **Name** — A human-readable label (e.g., "Acme Corp Production") - **Description** — What the key is for - **Metadata** — Optional JSON attached to the key (e.g., `{"customer_id": "acme-123"}`). Encrypted at rest and available to your Hot functions at runtime via `req.auth.service-key.meta`. Use this to pass customer context into your functions without requiring extra parameters. See [Caller Identity](/docs/mcp#caller-identity-hotrequest) for details. - **Permissions** — A granular permission map using the [permissions model](#permissions-model) below. - **Expiration** — Optional. Leave empty for a key that never expires. > **Resource type restriction:** Because service keys are customer-facing, they can only carry permissions for the `mcp`, `webhook`, `stream`, `event`, and `run` resource types. Administrative resource types (`project`, `build`, `context`, `key`, `session`, `env`) are rejected when creating a service key. The generated token is displayed **only once** at creation time. It has no `hot_` prefix, making it suitable for white-label integrations where your customers shouldn't see Hot branding. ### Managing Service Keys From the detail view, you can: - View the key's permissions, metadata, and timestamps - See when the key was last used - **Revoke** the key to immediately invalidate it Revoked and expired keys remain visible for audit purposes. Service keys can also be managed programmatically via the [Service Keys API](/docs/api#service-keys). ## Sessions **Sessions** are short-lived tokens with granular permissions. Use them when you need to grant temporary, narrowly scoped access — for example, giving a browser client read-only access to a specific stream. Sessions can only be created by API keys (not by other sessions or service keys). The session's permissions must be a subset of the parent API key's permissions. See the [Sessions API](/docs/api#sessions) for endpoints to create, list, and revoke sessions. ## Permissions Model API keys, service keys, and sessions all share the same permissions model. Permissions are a JSON map of resource URNs to action arrays: ```json { "mcp:weather": ["execute"], "event:*": ["create"] } ``` | Permission | Format | Description | |------------|--------|-------------| | **Full Access** | `{"*:*": ["*"]}` | Unrestricted access to all API endpoints | | **MCP** | `{"mcp:*": ["execute"]}` | Invoke MCP tools across all services | | **MCP (specific)** | `{"mcp:weather": ["execute"]}` | Invoke MCP tools in a specific service only | | **Events** | `{"event:*": ["create", "read"]}` | Publish and read events | | **Builds** | `{"build:*": ["create", "read"]}` | Upload builds and deploy them | | **Context Variables** | `{"context:*": ["create", "read", "update", "delete"]}` | Manage context variables | | **Webhooks** | `{"webhook:*": ["execute"]}` | Access webhook endpoints that require API key authentication | | **Webhooks (specific)** | `{"webhook:internal": ["execute"]}` | Webhook access for a specific service only | This allows you to create credentials narrowly scoped to just the capabilities needed—useful for giving an AI agent access to specific MCP tools, or restricting a webhook caller to a single service. ### Permissions Builder When creating or editing a restricted API key or service key, the [Hot App](/docs/app) dashboard provides an interactive **permissions builder** instead of requiring manual JSON editing. **Quick Presets** — One-click buttons to add common permission rules: | Preset | Adds | |--------|------| | Read Only | `run:*` → read, `stream:*` → read, `event:*` → read, `build:*` → read | | MCP Tools | `mcp:*` → execute | | Events | `event:*` → create, read | | Builds | `build:*` → create, read | | Context Vars | `context:*` → create, read, update, delete | | Webhooks | `webhook:*` → execute | Presets add rules to the builder — they don't replace existing rules. **Rule Builder** — Each rule has three parts: 1. **Resource Type** — Select a type from the dropdown (`mcp`, `webhook`, `stream`, `event`, `run`, `project`, `build`, `context`, `key`, `session`, `env`), or `All (*)` for a wildcard that covers every type. 2. **Path** — The resource path, usually `*` (all resources of that type) or a specific service name (e.g., `weather`). When the resource type is `All (*)`, the path is locked to `*`. 3. **Actions** — Check one or more actions: `create`, `read`, `update`, `delete`, `execute`, or `*` (all actions). The available actions depend on the resource type. Click **Add Rule** to add additional rules. Rules with no actions checked are silently excluded from the saved permissions. ### Stale Service Permissions Because MCP tools and webhook endpoints are defined by metadata in your source code, a service can be removed when code is redeployed without those definitions. If a credential has a permission referencing a service that is no longer deployed, the key list shows an amber warning badge next to that permission. The edit page displays a banner listing the stale services. Stale permissions are **preserved intentionally**—if the service is redeployed, the credential immediately works again without reconfiguration. You can remove stale permissions manually from the edit page if the service is permanently retired. See the [Hot API](/docs/api) documentation for the full API reference. --- Source: https://hot.dev/docs/app # Hot App The Hot App is a web-based management and observability platform for your Hot projects. It provides visibility into executions, events, and streams, along with project configuration and team management. **Access the Hot App:** - **Local development**: Run `hot dev` and open [http://localhost:4680](http://localhost:4680) - **Hot Cloud**: Sign up at [app.hot.dev](https://app.hot.dev) ## Navigation & Scope The sidebar provides navigation to all features of the Hot App. At the top are two key selectors: - **Organization** - Select which organization to view - **Environment** - Select which environment within that organization (e.g., development, staging, production) **These selectors control the scope of everything below.** When you select an organization and environment, all data throughout the app—runs, events, streams, projects, files, and more—is filtered to that specific context. From each selector dropdown, you can also access management screens: - **Organizations** - Create new organizations, manage users, configure teams, and handle billing - **Environments** - Create new environments, edit environment settings This scoping model keeps your data organized and allows you to easily switch between different projects or deployment stages. ## Dashboard The **Dashboard** is your home screen, providing an at-a-glance overview of your Hot environment: - **Issues banner** - A compact alert bar linking directly to failed runs, failed tasks, and unhandled events - **Hero metrics** - Four cards showing totals and status breakdowns for Runs, Tasks (including CUS), Events, and Streams - **Activity charts** - A scrollable grid of charts: run activity, run type distribution, task activity, CUS over time, event activity, event type distribution, stream activity, and stream composition - **Issues** - Expanded tables for failed runs, failed tasks, and unhandled events Use the filters at the top (project, time range, granularity) to scope the data. The dashboard auto-refreshes via server-sent events (SSE), and all charts and metrics update when filters change. ## Runs The **Runs** view shows all executions of your Hot code: - **Status** - Running, succeeded, failed, cancelled, or pending retry - **Type** - How it was triggered (call, event, schedule, run, eval, repl) - **Duration** - How long the run took - **Timestamp** - When the run started Click any run to see the full execution trace, including: - Input parameters - Each expression evaluated with timing - Return values or error details - File attachments (if any) - Parent/child run hierarchy You can **retry** failed runs or **rerun** any completed run directly from the detail view. ### Run states | State | Description | |-------|-------------| | `running` | Run is currently executing | | `succeeded` | Run completed successfully | | `failed` | Run failed with an error | | `cancelled` | Run was cancelled | | `pending_retry` | Run failed and is waiting for an automatic [retry](/docs/retries) | ## Tasks The **Tasks** view shows asynchronous and container-based jobs: - **Status** - Queued, running, completed, failed, timed out, or cancelled - **Duration** - How long the task ran - **CUS** - Compute units consumed by the task - **Timestamp** - When the task was created Click any task to see: - Task configuration and metadata - Timing breakdown (queue time, container pull, execution) - Call data from the task's execution - Container logs (for `::hot::box` tasks) - Stream graph showing the task in context with its originating run ### Task states | State | Description | |-------|-------------| | `queued` | Task is waiting to be picked up | | `running` | Task is currently executing | | `completed` | Task finished successfully | | `failed` | Task failed with an error | | `timed_out` | Task exceeded its timeout | | `cancelled` | Task was cancelled before completion | ## Events The **Events** view shows all events received by your Hot application: - **Event type** - The event name (e.g., `user:created`) - **Payload** - The event data - **Handled status** - Whether the event triggered a handler - **Timestamp** - When the event was received Click any event to see: - Full event payload - Which runs were triggered by this event - Event metadata Events are the primary way to trigger Hot functions. See [Events & Handlers](/docs/events) for more on defining event handlers. ## Streams **Streams** group related runs and events together, providing a unified view of a logical workflow or request. When a run triggers other runs or emits events, they're all linked under the same stream. - **Stream ID** - Unique identifier for the stream - **Run count** - Number of runs in this stream - **Event count** - Number of events in this stream - **Timeline** - Visual flow of runs and events Streams are especially useful for tracing complex workflows that span multiple function calls and events. ## Agents The **Agents** view shows all deployed [agents](/docs/agents) in your environment. Agents are typed groups of event handlers, schedules, and webhooks that share identity. - **Agents list** — Card grid showing each agent's name, namespace, description, tags, handler count, and project. Search by name, namespace, or project. A topology graph spanning all agents is shown at the top of the page. - **Agent Dashboard** — Click an agent to open its dashboard. The default **Graph** tab shows an interactive topology visualization of the agent's handlers, triggers, and event sends. Click any node to open an inspector sidebar with full details (namespace, source location, description, retry config, handled/sent events). Use the toolbar to toggle horizontal/vertical layout, open/close the inspector, zoom, or download the graph as a PNG. Additional tabs show **Handlers** (all linked handlers with trigger details), **Runs** (paginated run history), and **Streams** (related streams). - **Dashboard health widget** — The main Dashboard shows an Agent Health section with a health indicator per agent (green >95%, yellow 80–95%, red <80% success rate) and agent vs. non-agent run breakdown. Agents are defined using `agent` metadata on types and `meta {agent: TypeName}` on handler functions. See [Agents](/docs/agents) for the full definition and patterns. ## Files The **Files** view lets you browse and download files stored by your Hot functions: - View file metadata (size, content type, timestamps) - Download files directly - See which run created each file Files can be created in your Hot code using functions in the [`::hot::file`](https://hot.dev/pkg/hot.dev/hot-std/hot/file) namespace. ## Scheduled Runs The **Scheduled Runs** view shows all functions with schedule metadata: - **Schedule** - Cron expression defining when the function runs - **Next run** - When the function will next execute - **Recent runs** - History of scheduled executions See [Schedules](/docs/schedules) for more on defining scheduled functions. ## Event Handlers The **Event Handlers** view lists all functions that handle events: - **Event pattern** - Which events this handler responds to - **Function** - The handler function name - **Project** - Which project contains this handler ## MCP Services The **MCP Services** view lists all functions exposed as [Model Context Protocol](/docs/mcp) tools. Tools are grouped by **service**, showing tool name, description, file location, and project. Use the service filter to narrow the list. MCP tools are driven by `mcp` metadata in your source code — they're automatically registered on deploy and unregistered when removed. See [MCP Services](/docs/mcp) for how to define tools and configure the MCP endpoint. ## Webhooks The **Webhooks** view lists all functions exposed as [webhook endpoints](/docs/webhooks). Endpoints are grouped by **service**, showing method, path, function, auth mode, and description. Each service detail page shows the base URL for the webhook. Webhook endpoints are driven by `webhook` metadata in your source code — they're automatically registered on deploy and unregistered when removed. See [Webhooks](/docs/webhooks) for how to define endpoints and configure authentication. ## Alerts The **Alerts** view lets you configure monitoring and notifications for your Hot applications — run failures, deployment issues, and custom alerts from your code. Configure destinations (email, Slack, PagerDuty, webhook), channels, and subscriptions. See [Alerts](/docs/alerts) for the full documentation on channels, destinations, subscriptions, and sending alerts from code. ## Projects **Projects** represent deployed Hot applications. Each project shows: - **Active status** - Whether the project is currently active - **Builds** - Deployment history with the ability to deploy previous builds - **Documentation** - Auto-generated docs from your Hot code ## Context Variables **Context Variables** store configuration values accessible to your Hot code at runtime—things like API keys, feature flags, and environment-specific settings. Context variables can be defined at two levels: - **Environment level** - Shared across all projects in that environment - **Project level** - Specific to a single project, overrides environment-level values This hierarchy lets you set common defaults at the environment level while allowing individual projects to override specific values when needed. Access context values in your Hot code: ```hot api-key ::hot::ctx/get("API_KEY") debug-mode ::hot::ctx/get("DEBUG", false) ``` ### Local Development `hot dev` loads `hot/ctx.hot`. Use it to map values from `.env` to the context keys your Hot code reads: ```hot ::hot::run::ctx ns ::env ::hot::env ::hot::ctx/set({ "anthropic.api.key": ::env/get("ANTHROPIC_API_KEY", "") }) ``` `hot init` adds both `.env` and `hot/ctx.hot` to `.gitignore`. ### Deployed Projects Deployed projects do not use `hot/ctx.hot`. Add the same keys under **Context Variables** in the Hot App. Environment-level values apply to every project in the environment; project-level values override them. Hot stores these values encrypted in the database and loads them at runtime. ## Docs The **Docs** section provides auto-generated documentation for your deployed Hot projects: - Browse namespaces and functions - View function signatures and types - Search across your codebase - Explore package dependencies Each build includes documentation for both your project's source code and the specific package dependencies included in that build. This ensures the docs always match the exact versions deployed. The [hot-std](https://hot.dev/pkg/hot.dev/hot-std) standard library documentation is always available publicly at hot.dev, independent of any specific build. ## API Keys **API Keys** authenticate your requests to the Hot API with configurable permissions and fine-grained resource/action scoping. ## Service Keys **Service Keys** are customer-facing credentials with granular permissions, designed for external integrations and white-label use. See [Authentication](/docs/authentication) for full documentation on API keys, service keys, sessions, and the permissions model. ## Custom Domains **Custom Domains** let you map your own domain names to your Hot Dev environment for branded MCP, webhook, and API endpoints. See [Custom Domains](/docs/domains) for setup instructions, DNS configuration, and verification. ## Access Attribution Run and event detail pages display **access attribution** when the action was initiated via the API. This shows which credential (API key, service key, or session) made the request, along with the IP address, user agent, and HTTP request details. For actions initiated from the dashboard (e.g., re-running a function), the originating user is shown instead. ## Organizations & Teams Hot supports multi-tenant organization structures: - **Organizations** - Top-level accounts with billing and user management - **Teams** - Groups within an organization for access control - **Environments** - Isolated execution contexts (dev, staging, production) ### User Roles | Role | Permissions | |------|-------------| | Admin | Full access, manage users, billing, projects, settings | | Member | View and execute, limited configuration | --- Source: https://hot.dev/docs/cli # Hot CLI The `hot` command-line interface is your primary tool for developing, testing, and deploying Hot applications. ## Quick Reference | Command | Description | |---------|-------------| | `hot dev` | Start all services in development mode | | `hot run ` | Execute a single `.hot` file | | `hot eval ''` | Evaluate Hot code directly | | `hot test` | Run tests | | `hot check` | Analyze code for errors | | `hot deploy` | Deploy to Hot Cloud | ## Services ### hot dev Start all services for local development: ```bash hot dev ``` This starts: - **API** at `http://localhost:4681` — API for function calls and events - **App** at `http://localhost:4680` — Web dashboard for monitoring - **Worker** — Processes background jobs and event handlers - **Scheduler** — Runs scheduled functions Common options: ```bash hot dev --open # Open dashboard in browser hot dev --api.port 8080 # Custom API port hot dev --app.port 3000 # Custom App port hot dev --worker.threads 16 # More worker threads hot dev -d # Daemon mode (background) ``` Set `hot.dev.open true` in your `hot.hot` to always open the browser on start. ### hot api Run just the API server: ```bash hot api hot api --api.port 8080 --api.host 0.0.0.0 ``` ### hot app Run just the web application (dashboard): ```bash hot app hot app --app.port 3000 ``` ### hot worker Run just the background worker: ```bash hot worker hot worker --worker.threads 16 ``` ### hot scheduler Run just the job scheduler: ```bash hot scheduler ``` ## Running Code ### hot run Execute a single `.hot` file: ```bash hot run script.hot hot run path/to/file.hot hot run script.hot --value.format json # Output as JSON ``` ### hot eval Evaluate Hot code directly from the command line: ```bash hot eval 'add(1, 2)' hot eval '::my-app::hi/hello()' hot eval 'send("user:created", {id: 123})' hot eval '[1, 2, 3] |> map(mul(%, 2))' ``` Output format: ```bash hot eval '::my-app::get-user(1)' --value.format json ``` ### hot repl Start an interactive REPL session: ```bash hot repl ``` ## Testing & Development ### hot test Run tests: ```bash hot test # Run all tests hot test user # Run tests matching "user" hot test "user signup" # Run tests matching pattern ``` Tests are functions with `meta ["test"]` (or the equivalent `meta {test: true}`): ```hot should-add-numbers meta ["test"] fn () { assert-eq(add(1, 1), 2) } ``` ### hot check Analyze code for errors without executing: ```bash hot check # Check all project sources (pretty output) hot check path/to/file.hot # Check a specific file or directory hot check --check.format simple # One line per diagnostic (compact) hot check --check.format json # Pretty-printed LSP-shaped diagnostics hot check --check.format json-min # Minified JSON, ideal for CI / editor tooling ``` The `json` and `json-min` formats emit an array of LSP `Diagnostic` objects (`range`, `severity`, `code`, `source`, `message`, optional `file`). Diagnostic messages embed the same ariadne snippets shown in pretty mode when source is available. Exit code is `0` when no diagnostics are produced and `1` otherwise, so JSON mode is safe to drive from CI: ```bash hot check --check.format json-min > diagnostics.json || cat diagnostics.json ``` ### hot watch Watch for changes and continuously re-analyze. Supports the same `--check.format` flag as `hot check`, including `json` / `json-min` for editor / CI integration: ```bash hot watch # Pretty output, re-run on save hot watch --check.format json-min # Stream JSON diagnostics on every change ``` ### hot fmt Format Hot source files: ```bash hot fmt # Format all files hot fmt path/to/file.hot # Format specific file hot fmt --check # Check without writing (CI mode) ``` ## Build & Deploy ### hot build Create a build bundle from your project: ```bash hot build hot build --build.dir ./dist ``` ### hot builds List available builds. > **Note:** This command connects to Hot Cloud by default. To list builds from your local API, pass `--local` (requires `hot dev` or `hot api` to be running). ```bash hot builds # List builds on Hot Cloud hot builds --local # List builds from local API ``` ### hot compile Compile project source and create/update the live build: ```bash hot compile hot compile my-project ``` ### hot deploy Deploy a build to make it live. > **Note:** This command connects to Hot Cloud by default. To deploy locally, pass `--local` (requires `hot dev` or `hot api` to be running). ```bash hot deploy # Build and deploy to Hot Cloud hot deploy # Deploy a specific build to Hot Cloud hot deploy --local # Deploy via local API (for local dev or self-hosted) ``` ### hot cache Manage bytecode and package caches: ```bash hot cache clear # Clear all caches hot cache status # Show cache info ``` ## Project Management ### hot init Initialize Hot in a directory: ```bash hot init # Use current directory hot init my-app # Create my-app/ if needed, init there hot init path/to/app # Create nested path if needed, init there ``` The project name is taken from the directory name. Creates three things alongside your existing files: - `hot.hot` — Project configuration (project root) - `hot/src//hi.hot` — Starter file with tutorial - `.hot/` — Local data: cache, database, logs (gitignored) ### hot project Manage projects: ```bash hot project list hot project activate my-app hot project deactivate my-app ``` ### hot projects List all projects: ```bash hot projects ``` ### hot deps Manage dependencies: ```bash hot deps list # List all dependencies hot deps show # Show detailed dependency info hot deps add openai # Add a package hot deps remove openai # Remove a package hot deps update # Resolve and cache dependencies ``` ### hot context Manage encrypted context variables (secrets). > **Note:** This command connects to Hot Cloud by default. To manage local context variables, pass `--local` (requires `hot dev` or `hot api` to be running). ```bash hot context list # List all variables hot context get OPENAI_API_KEY # Get a variable hot context set OPENAI_API_KEY sk-xxx # Set a variable hot context delete OPENAI_API_KEY # Delete a variable hot context list --local # List from local API ``` ### hot conf Show current configuration or generate configuration templates: ```bash hot conf # Show resolved configuration hot conf generate # Generate minimal config template hot conf generate all # Generate full config with all options hot conf generate api # Generate API server config hot conf generate app # Generate App server config hot conf generate worker # Generate Worker config hot conf generate scheduler # Generate Scheduler config hot conf generate -o hot.hot # Write template to file ``` Available templates: - **(default)** — Minimal configuration for local development (~25 lines) - **all** — Full configuration with all available options (~180 lines) - **api** — API server configuration (for self-hosting) - **app** — App server configuration (for self-hosting) - **worker** — Worker configuration (for self-hosting) - **scheduler** — Scheduler configuration (for self-hosting) ## Tooling ### hot lsp Start the Language Server Protocol server (used by editors): ```bash hot lsp ``` ### hot completions Generate shell completions: ```bash hot completions bash > ~/.bash_completions/hot hot completions zsh > ~/.zfunc/_hot hot completions fish > ~/.config/fish/completions/hot.fish ``` ### hot ai Add AI coding support to help AI assistants understand Hot. This uses the open AGENTS.md and SKILL.md standards, which are supported by Cursor, Claude Code, GitHub Copilot, Windsurf, and many other AI coding tools. ```bash hot ai add # Add AGENTS.md + bundled skills to project hot ai add --global # Install skills to ~/.skills/ (available in all projects) ``` `hot ai add` installs the skill snapshots bundled with your Hot release, so it works offline and does not require Node or GitHub access. Available skills can vary by Hot version; use `hot ai list` to inspect the installed release. To install the latest public skills from the skills.sh ecosystem instead, use: ```bash npx skills add hot-dev/hot-skills ``` Files created: - `AGENTS.md` — AI agent instructions (passive context) - `.skills/hot-language/` — Detailed Hot language skill with references - `.skills/hot-ai-agents/` — Hot AI agent and SDK integration guidance Other commands: ```bash hot ai list # Show installed AI support files hot ai update # Update existing files to latest version ``` ## Info ### hot version Display version information: ```bash hot version ``` ### hot update Update Hot to the latest version: ```bash hot update ``` Install a specific release: ```bash hot update --version 1.4.0 ``` If your installed `hot` is too old to support `--version`, use the hosted installer script: ```bash curl -fsSL https://get.hot.dev/install.sh | sh -s -- --version 1.4.0 ``` Use `--force` to reinstall the current or requested version: ```bash hot update --force hot update --version 1.4.0 --force ``` This downloads and installs the requested version of Hot. If you're already on the selected version, it will let you know unless `--force` is set. ### hot help Display help information: ```bash hot help hot help dev hot help deploy ``` ## Global Options These options work with most commands: | Option | Description | |--------|-------------| | `-c, --conf ` | Configuration file(s) | | `--ctx ` | Context file(s) for variables | | `-p, --project ` | Project name to use | | `-s, --src.path ` | Source directory | | `-t, --test.path ` | Test directory | | `--engine.threads ` | Engine threads (default: 4) | | `--db.uri ` | Database connection URI | | `--log.level ` | Log level: off, trace, debug, error, warn, info | | `--log.target ` | Log output: stdout, file, none | | `--log.dir ` | Directory for log files | | `--log.rotation ` | Log rotation: hourly, daily, none (default: daily) | | `--log.retention ` | Number of log files to keep, 0 = keep all (default: 7) | | `--deploy.auto ` | Auto-deploy on CLI commands (default: true) | | `--emitter.type ` | Emitter: none, console, db (default: db in project, none otherwise) | | `--with-tests ` | Include test files in compile/check/watch (default: false) | | `--show-conf` | Show configuration and exit | ## Environment Variables The CLI reads configuration from environment variables prefixed with `HOT_`: ```bash export HOT_DB_URI="postgres://localhost/hot" export HOT_LOG_LEVEL="debug" export HOT_API_PORT="8080" ``` See [Configuration](/docs/configuration) for more details. --- Source: https://hot.dev/docs/configuration # Hot Configuration Configure your Hot project using the `hot.hot` configuration file. ## Creating a Configuration File The easiest way to create a `hot.hot` file is with `hot init`: ```bash hot init ``` This creates a minimal `hot.hot` file with sensible defaults for local development. To see all available configuration options, use: ```bash hot conf generate all ``` You can also generate component-specific templates for self-hosting: ```bash hot conf generate api # API server config hot conf generate app # App server config hot conf generate worker # Worker config hot conf generate scheduler # Scheduler config ``` ## Overview The `hot.hot` file is the central configuration for your Hot project. It defines: - **Projects** - Named project configurations with source paths and dependencies - **Dependencies** - External packages your project uses - **Settings** - Global defaults like the active project, profile, and remote - **Services** - Database, Redis, logging, and other infrastructure settings ## Basic Structure A minimal `hot.hot` file (created by `hot init`) looks like: ```hot // hot.hot - Project Configuration File ::hot::conf ns ::env ::hot::env // Profile and Project Settings hot.set.profile "local-dev" hot.set.project "my-app" hot.set.remote "hot-dev" // Local Development Profile hot.profile.local-dev.user.email "dev@example.com" hot.profile.local-dev.org.slug "dev" hot.profile.local-dev.env.name "development" // Remote API (hot.dev) hot.remote.hot-dev.url ::env/get("HOT_API_URL", "https://api.hot.dev") hot.remote.hot-dev.key ::env/get("HOT_API_KEY", "") // Project Configuration hot.project.my-app.src.paths ["./hot/src"] hot.project.my-app.test.paths ["./hot/test"] hot.project.my-app.deps {} ``` That's all you need for local development. Database, logging, and other services use sensible defaults. For production or advanced configuration, add settings as needed: ```hot // Database Configuration (defaults to local SQLite) hot.db.uri ::env/get("HOT_DB_URI", "sqlite:./.hot/db/hot.sqlite.db") // Logging Configuration hot.log.level ::env/get("HOT_LOG_LEVEL", "info") hot.log.target ::env/get("HOT_LOG_TARGET", "stdout") // Dependencies hot.project.my-app.deps { "hot.dev/anthropic": "0.9.0", "hot.dev/openai": "0.9.0" } ``` Run `hot conf generate all` to see all available options. ## Minimum Version Requirement Use `hot.min-version` to specify the minimum Hot version required for your project: ```hot hot.min-version "1.0.0" ``` When set, Hot will check this requirement at startup and display a clear error if the requirement is not met: ``` Version requirement not met: Hot version 1.0.0 is required, but you are running 0.11.0 This project requires Hot 1.0.0 or later. ``` This is useful for: - **Team coordination** - Ensure all team members are on a compatible version - **CI/CD** - Fail fast with a clear message before builds - **Feature requirements** - When your code uses features from a specific Hot version ## Logging Configuration | Setting | Description | Default | |---------|-------------|---------| | `hot.log.level` | Log level: trace, debug, info, warn, error, off | `info` | | `hot.log.target` | Output target: stdout, file, none | `stdout` | | `hot.log.dir` | Directory for log files (when target is file) | `.hot/log` | | `hot.log.rotation` | File rotation: hourly, daily, none | `daily` | | `hot.log.retention` | Number of log files to keep (0 = keep all) | `7` | When `log.target` is set to `file`, logs are written to the configured directory with automatic rotation and cleanup based on the retention setting. ## Trusted Proxy Client IPs Client IP forwarding stays in compatibility mode by default: the API uses the first `X-Forwarded-For` value, then `X-Real-IP`, as before. When either header is present in compatibility mode, the API logs a warning because clients can spoof these values when the service is directly reachable. Self-hosted deployments can opt into validated proxy identity: ```hot hot.network.client-ip.trusted-proxy true hot.network.client-ip.header "x-forwarded-for" hot.network.client-ip.trusted-proxies ["10.0.0.0/8", "2001:db8:1234::/48"] ``` List every proxy CIDR that may connect directly to the API or appear at the trusted end of the forwarding chain. When enabled, startup fails if the list is empty or invalid. Forwarding headers from untrusted peers, or malformed header values, are ignored in favor of the direct peer address. `trusted-proxies` also accepts a comma-delimited string, which is useful for an environment-backed deployment setting. ## Configuration Format Hot configuration uses a dotted notation where each setting is a separate assignment: ```hot // Setting a simple value hot.log.level "info" // Setting from environment with default hot.api.port Int(::env/get("HOT_API_PORT", "4681")) // Setting a list hot.project.my-app.src.paths ["./hot/src", "./lib"] // Setting a map (for dependencies) hot.project.my-app.deps { "hot.dev/anthropic": "0.9.0" } ``` ## Sections - **[Dependencies](/docs/configuration/dependencies)** - How to declare and manage package dependencies - **[Projects](/docs/configuration/projects)** - Configuring multiple projects in one workspace --- Source: https://hot.dev/docs/configuration/dependencies # Dependencies Hot uses a flexible dependency system that supports local paths, Git repositories, and the Hot package registry. ## Dependency Format Dependencies are declared using the `deps` setting with a map of package coordinates to dependency specifications: ```hot hot.project.my-app.deps { "hot.dev/package-name": "1.0.0" } ``` ### Package Coordinates Package coordinates use the format `org/package-name`: - `hot.dev/anthropic` - The Anthropic package from Hot Dev - `hot.dev/aws-s3` - AWS S3 bindings - `my-org/my-package` - A custom package ## Dependency Specifications ### Version String (Recommended) The simplest way to specify a dependency is with a version string: ```hot hot.project.my-app.deps { "hot.dev/anthropic": "1.0.0", "hot.dev/openai": "2.1.3" } ``` This fetches the exact version from the Hot package registry. A version is always required—there is no "latest" resolution. > **Note:** The `hot.dev` registry currently hosts official Hot packages. Support for publishing your own packages to `pkg.hot.dev` is coming soon! In the meantime, you can share packages via Git repositories (see [Git Dependencies](#git-dependencies) below). ### Specification Object For more control, use a specification object. The resolver follows this priority: 1. **Local path exists** → Use local 2. **Local path doesn't exist, Git specified** → Clone from Git 3. **Only Git specified** → Clone from Git 4. **Empty spec `{}`** → Resolve from default locations ### Local Dependencies Point to a local directory: ```hot hot.project.my-app.deps { "hot.dev/my-lib": { "local": "./libs/my-lib" } } ``` ### Git Dependencies Clone from a Git repository: ```hot hot.project.my-app.deps { "hot.dev/stripe": { "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/stripe", // Path within the repo "tag": "v0.1.0" // Or use "branch": "main" } } ``` ### Local with Git Fallback The recommended pattern for packages in a monorepo - prefer local during development, fall back to Git for distribution: ```hot hot.project.my-app.deps { "hot.dev/aws-core": { "local": "../aws-core", "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/aws-core" } } ``` This means: - If `../aws-core` exists, use it (great for local development) - If not, clone from Git (works for published packages) ### Default Resolution An empty spec `{}` resolves from standard locations: ```hot hot.project.my-app.deps { "hot.dev/anthropic": {} } ``` Resolution order: 1. `$HOT_HOME/pkg/` 2. `./hot/pkg/` (development) 3. Executable-relative `resources/pkg/` 4. System install paths (`/usr/local/share/hot/pkg/` on macOS) ## Transitive Dependencies Hot automatically resolves transitive dependencies. If you depend on `aws-s3`, and `aws-s3` depends on `aws-core`, you don't need to declare `aws-core` yourself. ```hot // You only need to declare aws-s3 hot.project.my-app.deps { "hot.dev/aws-s3": { "local": "./hot/pkg/aws-s3" } } // aws-core is automatically included via aws-s3's pkg.hot ``` ### Project Overrides Your project-level deps take precedence over transitive deps. This lets you use a local version of a transitive dependency: ```hot hot.project.my-app.deps { "hot.dev/aws-s3": { "local": "./hot/pkg/aws-s3" }, // Override aws-core to use your local modified version "hot.dev/aws-core": { "local": "./my-modified-aws-core" } } ``` ## Dependency Specification Fields When using a specification object, these fields are available: | Field | Type | Description | |-------|------|-------------| | `local` | String | Local filesystem path (relative or absolute) | | `git` | String | Git repository URL (HTTPS or SSH) | | `branch` | String | Git branch name (mutually exclusive with `tag`) | | `tag` | String | Git tag or commit SHA (mutually exclusive with `branch`) | | `path` | String | Path within the Git repository (for monorepos) | ## Examples ### Minimal Project (Registry) ```hot hot.project.my-app.src.paths ["./src"] hot.project.my-app.deps { "hot.dev/anthropic": "1.0.0" } hot.set.project "my-app" ``` ### Local Development ```hot hot.project.my-app.src.paths ["./src"] hot.project.my-app.deps { "hot.dev/anthropic": { "local": "./hot/pkg/anthropic" } } hot.set.project "my-app" ``` ### Full-Featured Project ```hot // Project settings hot.set.project "production-api" // Source and test paths hot.project.production-api.src.paths ["./src", "./lib"] hot.project.production-api.test.paths ["./test"] hot.project.production-api.test.capture true // Dependencies hot.project.production-api.deps { // Registry packages (recommended for published packages) "hot.dev/anthropic": "1.0.0", "hot.dev/openai": "2.1.3", // Local development packages "hot.dev/my-lib": { "local": "./hot/pkg/my-lib" }, // Git-based package with specific version "hot.dev/stripe": { "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/stripe", "tag": "v0.2.0" }, // Custom internal package "my-org/internal-utils": { "git": "git@github.com:my-org/hot-packages.git", "path": "packages/internal-utils", "branch": "main" } } ``` --- Source: https://hot.dev/docs/configuration/projects # Projects A Hot workspace can contain multiple projects, each with its own source paths and dependencies. ## Project Configuration Project settings use dotted notation: ```hot // Source paths hot.project.my-app.src.paths ["./hot/src"] // Test configuration hot.project.my-app.test.paths ["./hot/test"] hot.project.my-app.test.capture true // Dependencies hot.project.my-app.deps { "hot.dev/anthropic": { "local": "./hot/pkg/anthropic" } } ``` ## Configuration Fields ### src.paths Defines where your Hot source files are located: ```hot hot.project.my-app.src.paths ["./hot/src", "./lib"] ``` - List of directories containing `.hot` files - All paths are relative to the `hot.hot` file location ### test.paths Directories containing test files: ```hot hot.project.my-app.test.paths ["./hot/test"] ``` ### test.capture Whether to capture stdout during tests (default: `true`): ```hot hot.project.my-app.test.capture true ``` ### deps Package dependencies (see [Dependencies](/docs/configuration/dependencies) for full details): ```hot hot.project.my-app.deps { "hot.dev/anthropic": { "local": "./hot/pkg/anthropic" } } ``` ## Multiple Projects You can define multiple projects in one workspace: ```hot // Development project with local packages hot.project.dev.src.paths ["./hot/src"] hot.project.dev.test.paths ["./hot/test"] hot.project.dev.deps { "hot.dev/stripe": { "local": "./hot/pkg/stripe" } } // Production project with pinned versions hot.project.prod.src.paths ["./hot/src"] hot.project.prod.test.paths ["./hot/test"] hot.project.prod.deps { "hot.dev/stripe": { "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/stripe", "tag": "v1.0.0" } } // Set default project hot.set.project "dev" ``` Switch between projects: ```bash hot run -p prod hot test -p dev ``` ## Default Project Set the default project with: ```hot hot.set.project "my-project-name" ``` This project is used when no `-p/--project` flag is specified. ## Store Configuration `::hot::store` persists data in the main Hot database and is always scoped to the current organization and environment. Unlike `::hot::file` direct mode, store access needs a Hot project/runtime context with a migrated database and an active environment. The local backend defaults to SQLite. You can select the store backend in `hot.hot`; `HOT_STORE_TYPE` takes precedence when set: ```hot hot.store.type ::env/get("HOT_STORE_TYPE", "sqlite") // "sqlite" or "postgres" ``` Store maps can also use embeddings for semantic search. These defaults are used when a map requests `embedding: EmbeddingOptions.Default`: ```hot hot.store.embedding.provider ::env/get("HOT_STORE_EMBEDDING_PROVIDER", "local") hot.store.embedding.model ::env/get("HOT_STORE_EMBEDDING_MODEL", "bge-base-en-v1.5") hot.store.embedding.field ::env/get("HOT_STORE_EMBEDDING_FIELD", "content") hot.store.models.path ::env/get("HOT_STORE_MODELS_PATH", ".hot/models") ``` ## Project Naming Project names: - Must be valid Hot identifiers - Can contain letters, numbers, and hyphens - Cannot start with a number - Are case-sensitive Good names: - `my-app` - `production-api` - `dev` - `myProject` ## Directory Structure Hot lives alongside your existing code. `hot init` adds `hot.hot` to the project root, Hot source files go in `hot/`, and local data (cache, database, logs) goes in `.hot/`: ``` my-project/ ├── src/ # Your existing code (any language) ├── package.json # Your existing config files ├── hot.hot # Hot configuration (project root) ├── hot/ │ ├── src/ # Hot source files │ │ └── my-app/ │ │ └── main.hot │ ├── test/ # Hot test files │ │ └── my-app/ │ │ └── test_main.hot │ └── pkg/ # Local packages │ ├── anthropic/ │ └── openai/ └── .hot/ # Local data (gitignored) ├── cache/ └── db/ ``` --- Source: https://hot.dev/docs/packages # Hot Package Creation Create reusable Hot packages to share code across projects and with the community. ## Overview A Hot package is a self-contained collection of Hot code with: - A `pkg.hot` manifest file - Source files in a `src/` directory - Optional test files in a `test/` directory - Dependencies on other packages ## Package Structure ``` my-package/ ├── pkg.hot # Package manifest ├── src/ │ └── my-package/ │ ├── main.hot # Package code │ └── utils.hot └── test/ └── my-package/ └── test_main.hot ``` ## Sections - **[Package Manifest](/docs/packages/manifest)** - The `pkg.hot` file format - **[Package Dependencies](/docs/packages/dependencies)** - Declaring package dependencies - **[Publishing Packages](/docs/packages/publishing)** - Sharing packages with others ## Quick Start Create a minimal package: ```bash mkdir -p my-package/src/my-package mkdir -p my-package/test ``` Create `my-package/pkg.hot`: ```hot ::hot::pkg ns hot.pkg.my-package { name: "my-package", version: "0.1.0", description: "My awesome Hot package", author: "Your Name", email: "you@example.com", url: "https://github.com/you/my-package", license: "MIT", deps: { "hot.dev/hot-std": {} }, src-paths: ["src/"], test-paths: ["test/"] } ``` Create `my-package/src/my-package/main.hot`: ```hot ::my-package ns greet meta { doc: "Return a greeting message" } fn (name: Str): Str { `Hello, ${name}!` } ``` Now you can use your package in a project by adding it to deps: ```hot // In your hot.hot hot.project.my-app.deps { "my-org/my-package": { "local": "./my-package" } } ``` > **Tip**: Use `hot init` to create a new project with a properly configured `hot.hot` file, then add your package to the deps. --- Source: https://hot.dev/docs/packages/manifest # Package Manifest The `pkg.hot` file defines your package's metadata and dependencies. ## Format ```hot ::hot::pkg ns hot.pkg. { name: "package-name", version: "0.1.0", description: "Package description", author: "Author Name", email: "author@example.com", url: "https://github.com/org/package", license: "MIT", deps: { // dependencies }, src-paths: ["src/"], test-paths: ["test/"] } ``` ## Fields ### Required Fields | Field | Type | Description | |-------|------|-------------| | `name` | String | Package name (should match directory name) | | `version` | String | Semantic version (e.g., "1.0.0") | | `description` | String | Brief description of the package | | `deps` | Map | Package dependencies | | `src-paths` | Vec | Directories containing source files | ### Optional Fields | Field | Type | Description | |-------|------|-------------| | `author` | String | Package author name | | `email` | String | Contact email | | `url` | String | Package homepage or repository URL | | `license` | String | License identifier (e.g., "MIT", "Apache-2.0") | | `org` | String | Organization identifier | | `tags` | Vec | Broad browse categories for package discovery | | `test-paths` | Vec | Directories containing test files | | `hot-min-version` | String | Minimum Hot version required (e.g., "1.0.0") | ## Examples ### Minimal Package ```hot ::hot::pkg ns hot.pkg.my-utils { name: "my-utils", version: "0.1.0", description: "Utility functions", deps: { "hot.dev/hot-std": {} }, src-paths: ["src/"] } ``` ### Full Package ```hot ::hot::pkg ns hot.pkg.aws-s3 { name: "aws-s3", version: "0.1.0", description: "AWS S3 API bindings for Hot", author: "Hot Dev", email: "support@hot.dev", url: "https://hot.dev", license: "MIT", tags: ["cloud"], hot-min-version: "1.0.0", deps: { "hot.dev/hot-std": {}, "hot.dev/aws-core": { "local": "../aws-core", "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/aws-core" } }, src-paths: ["src/"], test-paths: ["test/"] } ``` ## Namespace Convention The package manifest lives in the `::hot::pkg` namespace. The variable name should be `hot.pkg.` where `` matches your package name. ```hot ::hot::pkg ns hot.pkg.my-package { // Variable name matches package name: "my-package", // name field matches too // ... } ``` ## Version Format Use semantic versioning (SemVer): - `1.0.0` - Initial stable release - `1.0.1` - Patch release (bug fixes) - `1.1.0` - Minor release (new features, backwards compatible) - `2.0.0` - Major release (breaking changes) For pre-release versions: - `0.1.0` - Early development - `1.0.0-alpha.1` - Alpha release - `1.0.0-beta.1` - Beta release - `1.0.0-rc.1` - Release candidate ## Minimum Hot Version Use `hot-min-version` to specify the minimum Hot version your package requires: ```hot hot.pkg.my-package { name: "my-package", version: "1.0.0", hot-min-version: "1.0.0", // Requires Hot 1.0.0 or later // ... } ``` When a user tries to install or use your package with an older Hot version, they'll see: ``` Package 'my-package' requires Hot 1.0.0: Hot version 1.0.0 is required, but you are running 0.11.0 ``` This is useful when your package uses language features or standard library functions introduced in a specific Hot version. ## Tags Use `tags` for broad package directory categories, not every capability, API method, or keyword a package supports. Package search indexes package names, descriptions, namespace names, function/type names, and doc summaries, so detailed terms belong in documentation rather than the top-level category list. Use lowercase, hyphenated IDs from the approved category set: - `ai` - `automation` - `cloud` - `database` - `documents` - `email` - `hot` - `media` - `messaging` - `payments` - `protocols` Most packages should have one tag. Use two only when both browse paths are important, such as `["cloud", "email"]` for an AWS email package or `["ai", "protocols"]` for an MCP package. --- Source: https://hot.dev/docs/packages/dependencies # Package Dependencies Declare your package's dependencies on other packages. ## Dependency Format Package dependencies use the same format as project dependencies: ```hot deps: { "org/package-name": "1.0.0" } ``` Or with a specification object for more control: ```hot deps: { "org/package-name": { /* spec */ } } ``` ## Dependency Types ### Registry Packages For published packages, use a version string. A version is always required—there is no "latest" resolution: ```hot deps: { "hot.dev/anthropic": "1.0.0" } ``` > **Note:** The `hot.dev` registry currently hosts official Hot packages. Support for publishing your own packages to `pkg.hot.dev` is coming soon! In the meantime, share packages via Git repositories. ### Local Sibling Packages For packages in a monorepo that depend on each other, use local with git fallback: ```hot deps: { "hot.dev/aws-core": { "local": "../aws-core", "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/aws-core" } } ``` This pattern: - Uses `../aws-core` during local development (fast iteration) - Falls back to Git when the local path doesn't exist (distribution) ### Git-Only Dependencies For external packages: ```hot deps: { "other-org/their-package": { "git": "git@github.com:other-org/hot-packages.git", "path": "packages/their-package", "tag": "v1.0.0" } } ``` ## Resolution Behavior When your package is used as a dependency: 1. **Project overrides apply** - If the user's `hot.hot` specifies a different source for any of your deps, their spec takes precedence 2. **Transitive resolution** - Your deps are automatically resolved for the user 3. **Local fallback works** - If your `local` path exists relative to where the package is, it's used ### Example: User Perspective Your package `aws-s3` has this in its `pkg.hot`: ```hot deps: { "hot.dev/aws-core": { "local": "../aws-core", "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/aws-core" } } ``` **Scenario 1: User has both packages locally** ```hot // User's hot.hot deps: { "hot.dev/aws-s3": { "local": "./hot/pkg/aws-s3" }, "hot.dev/aws-core": { "local": "./hot/pkg/aws-core" } } ``` Result: Both use local paths (user's override for `aws-core`). **Scenario 2: User only has aws-s3 locally** ```hot // User's hot.hot deps: { "hot.dev/aws-s3": { "local": "./hot/pkg/aws-s3" } } ``` Result: `aws-s3` uses local, `aws-core` resolved via git fallback from `aws-s3`'s deps. **Scenario 3: User uses git for everything** ```hot // User's hot.hot deps: { "hot.dev/aws-s3": { "git": "git@github.com:hot-dev/hot.git", "path": "hot/pkg/aws-s3" } } ``` Result: `aws-s3` cloned from git, `aws-core` also resolved from git. ## Best Practices ### 1. Use Version Strings for Published Packages ```hot deps: { "hot.dev/anthropic": "1.0.0", "hot.dev/openai": "2.1.3" } ``` ### 2. Use Local+Git for Monorepo Packages ```hot deps: { "hot.dev/sibling-package": { "local": "../sibling-package", "git": "git@github.com:your-org/pkg.git", "path": "path/to/sibling-package" } } ``` ### 3. Keep Dependencies Minimal Only declare direct dependencies. Don't re-declare transitive dependencies unless you need to override their source. ## Circular Dependencies Circular dependencies are not allowed. If package A depends on B, and B depends on A, resolution will fail with an error. Structure your packages to avoid cycles: - Extract shared code into a `core` package - Use dependency inversion (depend on interfaces, not implementations) --- Source: https://hot.dev/docs/packages/publishing # Publishing Packages Share your Hot packages with others. ## Distribution Methods ### Git Repository The most common way to distribute packages is via Git: 1. **Push to a Git repository** (GitHub, GitLab, etc.) 2. **Users add the dependency** with git coordinates: ```hot deps: { "your-org/your-package": { "git": "git@github.com:your-org/hot-packages.git", "path": "packages/your-package", "tag": "v1.0.0" } } ``` ### Monorepo Structure For multiple packages, use a monorepo: ``` hot-packages/ ├── packages/ │ ├── package-a/ │ │ ├── pkg.hot │ │ └── src/ │ ├── package-b/ │ │ ├── pkg.hot │ │ └── src/ │ └── package-c/ │ ├── pkg.hot │ └── src/ └── README.md ``` Users reference individual packages via `path`: ```hot deps: { "your-org/package-a": { "git": "git@github.com:your-org/hot-packages.git", "path": "packages/package-a" }, "your-org/package-b": { "git": "git@github.com:your-org/hot-packages.git", "path": "packages/package-b" } } ``` ## Versioning ### Git Tags Use Git tags for versioning: ```bash git tag v1.0.0 git push origin v1.0.0 ``` Users can pin to specific versions: ```hot deps: { "your-org/your-package": { "git": "...", "tag": "v1.0.0" } } ``` ### Branch-Based Development For bleeding edge, users can track a branch: ```hot deps: { "your-org/your-package": { "git": "...", "branch": "main" } } ``` ⚠️ **Warning**: Branch deps are updated on each resolution, which can cause unexpected changes. ## Package Checklist Before publishing, ensure your package: - [ ] Has a complete `pkg.hot` with all metadata - [ ] Has a README.md explaining usage - [ ] Has working tests (`hot test`) - [ ] Passes checks (`hot check`) - [ ] Has appropriate license - [ ] Has minimal, necessary dependencies - [ ] Uses semantic versioning ## README Template Create a `README.md` in your package directory: ```markdown # Package Name Brief description of what this package does. ## Installation Add to your `hot.hot`: \`\`\`hot deps: { "your-org/your-package": { "git": "git@github.com:your-org/hot-packages.git", "path": "packages/your-package", "tag": "v1.0.0" } } \`\`\` ## Usage \`\`\`hot // Import and use greet ::your-package/greet result greet("World") // => "Hello, World!" \`\`\` ## API Reference ### `greet(name: Str): Str` Returns a greeting message. ## License MIT ``` ## Hot Package Registry The Hot package registry at `pkg.hot.dev` hosts official Hot packages. Use exact version strings to specify dependencies: ```hot deps: { "hot.dev/stripe": "1.0.0" } ``` > **Note:** Support for publishing your own packages to the registry is coming soon! In the meantime, share packages via Git repositories. --- Source: https://hot.dev/docs/ci-cd # CI/CD Automate testing and deployment of your Hot projects in continuous integration pipelines. ## GitHub Actions The [`hot-dev/setup-hot`](https://github.com/hot-dev/setup-hot) action installs the Hot CLI on GitHub Actions runners. It handles OS/architecture detection, downloads the correct installer, and verifies the installation. ### Basic Deploy Deploy to Hot Cloud on every push to `main`: ```yaml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hot-dev/setup-hot@v1 - run: hot deploy env: HOT_API_KEY: ${{ secrets.HOT_API_KEY }} ``` ### Test and Deploy Run type checking and tests before deploying: ```yaml name: Test and Deploy on: push: branches: [main] jobs: test-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hot-dev/setup-hot@v1 - run: hot check - run: hot test - run: hot deploy env: HOT_API_KEY: ${{ secrets.HOT_API_KEY }} ``` ### Deploy on Release Deploy only when a GitHub release is published: ```yaml name: Deploy on Release on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hot-dev/setup-hot@v1 - run: hot deploy env: HOT_API_KEY: ${{ secrets.HOT_API_KEY }} ``` ### Pin a Specific Version Lock the Hot CLI to a specific version for reproducible builds: ```yaml - uses: hot-dev/setup-hot@v1 with: version: '1.2.3' ``` ### Pass the API Key Through the Action Instead of setting `HOT_API_KEY` on each step, pass it directly to the action: ```yaml - uses: hot-dev/setup-hot@v1 with: api-key: ${{ secrets.HOT_API_KEY }} - run: hot deploy ``` ### Action Inputs | Input | Description | Required | Default | |-------|-------------|----------|---------| | `version` | Hot version to install (e.g. `1.2.3`) | No | `latest` | | `api-key` | Hot API key. Alternative to setting `HOT_API_KEY` env var | No | | ### Supported Runners | Runner | OS | Architecture | |--------|-----|-------------| | `ubuntu-latest` | Linux | x86_64 | | `ubuntu-24.04-arm` | Linux | arm64 | | `macos-latest` | macOS | arm64 | | `macos-13` | macOS | x86_64 | ## Other CI Providers For CI systems without a dedicated action, install Hot with the shell installer and run commands directly. ### GitLab CI ```yaml deploy: image: ubuntu:latest script: - curl -fsSL https://get.hot.dev/install.sh | sh - hot check - hot test - hot deploy only: - main ``` ### Generic Script Any CI environment that runs bash can install and use Hot: ```bash curl -fsSL https://get.hot.dev/install.sh | sh hot check hot test hot deploy ``` Set `HOT_API_KEY` as a secret/environment variable in your CI provider's settings. ## CI Best Practices ### Run Checks Before Deploying Use `hot check` and `hot test` as gates before deployment. Add `hot fmt --check` to enforce consistent formatting: ```bash hot fmt --check # Fails if files aren't formatted hot check # Type checking hot test # Run tests hot deploy # Only if everything passes ``` ### Manage Secrets Store your `HOT_API_KEY` as an encrypted secret in your CI provider — never commit it to your repository. - **GitHub Actions** — Add `HOT_API_KEY` under **Settings → Secrets and variables → Actions** - **GitLab CI** — Add under **Settings → CI/CD → Variables** (mask and protect it) - **Other providers** — Use the provider's secret/environment variable management Your `hot.hot` config reads the key automatically: ```hot hot.remote.hot-dev.key ::env/get("HOT_API_KEY", "") ``` ### Pin Versions for Stability In production pipelines, pin the Hot CLI version to avoid unexpected changes: ```yaml - uses: hot-dev/setup-hot@v1 with: version: '1.2.3' ``` Update the pinned version intentionally when you're ready to upgrade. --- Source: https://hot.dev/docs/migrations # Migrations and Upgrades This guide covers upgrading an existing Hot project and database between Hot releases. ## Updating Hot Update to the latest published Hot release: ```bash hot update ``` If your installed `hot` supports pinned updates, install a specific Hot version: ```bash hot update --version 2.3.0 ``` For older `hot` binaries that do not support `hot update --version`, use the hosted installer script instead. macOS / Linux: ```bash curl -fsSL https://get.hot.dev/install.sh | sh -s -- --version 2.3.0 ``` Windows PowerShell: ```powershell $env:HOT_VERSION = "2.3.0"; irm https://get.hot.dev/install.ps1 | iex ``` Pinned installs are useful when you need to finish database migrations with an older release line before moving to a newer major version. ## Upgrading to Hot 2.6 Hot 2.6 completes the move to one error idiom and removes the legacy flow-result-modifier syntax: - **`::hot::lang/try` and `::hot::lang/try-call` are removed.** Expected failures are `Result.Err` values — branch with `is-err` / `if-err`; use `OnErr.Preserve` for fan-out isolation and a task boundary (`::hot::task/start` + `await`) to supervise untrusted work. See [Error Handling](/docs/language/errors). - **The `|map`, `|vec`, and `|one` flow result modifiers are removed** — annotate the binding or return type instead. `All` / `All` collect all results (`x: All cond { ... }`); any other type on a collect-all flow takes the single final value (`x: Int parallel { ... }`). See [Flows](/docs/language/flows). ## Upgrading to Hot 2.3 Hot 2.3 includes a breaking cleanup to the public `Failure` and `Cancellation` payload fields. Replace direct field access as follows: - `failure.$msg` → `failure.msg` - `failure.$err` → `failure.err` - `cancellation.$msg` → `cancellation.msg` - `cancellation.$data` → `cancellation.data` ## Upgrading to Hot 2.2 No code changes were required for 2.2 itself. The idioms it introduced are now the standard forms — and, as of Hot 2.6, the only forms: the legacy syntax that 2.2 still tolerated (`|map`/`|vec`/`|one` modifiers, `try-call`) is removed, see [Upgrading to Hot 2.6](#upgrading-to-hot-26). The version bump invalidates bytecode and AST cache entries from older versions; they rebuild automatically on first run. 2.2 added: - `All` / `All` annotations for flow result shape — see [Flows](/docs/language/flows). - `OnErr.Force` / `OnErr.Preserve` disposition for map-shaped higher-order functions — see [Error Handling](/docs/language/errors). ## Upgrading from Hot 1.x to Hot 2 Hot 2 ships a clean baseline schema and does not migrate a Hot 1.x database in place. The public upgrade path covers local SQLite projects; Hot Cloud's v1→v2 data backfill lives in the private cloud repository. ### Before you upgrade Back up your database before changing major versions. `hot db port-v1-to-v2` writes its own backup of the v1 SQLite file alongside the original, but a separate copy is still good practice. Check the installed version before each phase: ```bash hot version ``` ### SQLite (local development) If you do not need to preserve your local data, the simplest path is to delete the SQLite file and let Hot 2 create a fresh one: ```bash rm .hot/db/hot.sqlite.db hot db migrate ``` To preserve your data, run the SQLite porter: ```bash hot update hot db port-v1-to-v2 ``` `hot db port-v1-to-v2` writes a backup of your v1 file alongside it (named `hot.sqlite.db.v1.bak.`), applies the Hot 2 baseline migrations to a fresh file at the original path, and copies your user-data rows from the backup using SQLite's `ATTACH DATABASE`. The resulting v2 file's schema is byte-identical to a fresh `hot init`. Tables Hot 2 pre-populates with seed rows (statuses, roles, alert channels, scheduler state) are not copied; v1-only tables (`subscription_plan`, `subscription`, `store`) have no Hot 2 destination and are reported as dropped. The v1 backup file is preserved; remove it manually when you no longer need it. --- Source: https://hot.dev/docs/editor # VS Code & LSP Hot provides first-class editor support through a Language Server Protocol (LSP) implementation and VS Code extension. ## VS Code Extension The Hot VS Code extension provides a complete development experience: - **Syntax highlighting** — Full Hot language support - **Diagnostics** — Real-time error checking and warnings - **Autocomplete** — Function, type, and namespace completion - **Hover information** — Type info and documentation - **Go to definition** — Navigate to function and type definitions ### Installation Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=hot-dev.hot): 1. Open VS Code 2. Go to Extensions (Cmd+Shift+X / Ctrl+Shift+X) 3. Search for "Hot" and choose the `Hot` extension from `hot-dev` 4. Click Install Or install from the command line: ```bash code --install-extension hot-dev.hot ``` **Cursor, Windsurf & other VS Code-compatible editors:** Search for "Hot" by `hot-dev` in the Extensions panel ([also on Open VSX](https://open-vsx.org/extension/hot-dev/hot)). ### Features #### Syntax Highlighting Hot files (`.hot`) are automatically recognized with full syntax highlighting: - Declaration keywords (`fn`, `type`, `enum`, `ns`) - Control keywords (`lazy`, `do`) - Flow keywords (`cond`, `parallel`, `serial`, `match`, `cond-all`, `match-all`) - Special syntax (`|>` pipe, `=>` branch, `->` coercion, `|` union) - Metadata annotations (`meta`) - Strings, block strings, template strings, and block template strings - Numbers and booleans - Comments - Namespaces and function paths #### Diagnostics See errors and warnings in real-time as you type: - Type mismatches - Undefined functions or variables - Syntax errors - Unused variables (warnings) Errors appear as red squiggles in the editor and in the Problems panel (Cmd+Shift+M). #### IntelliSense Get intelligent code completion as you type: - Function names with signatures - Type names and fields - Namespace paths - Local variables - Core library functions Press `Ctrl+Space` to trigger autocomplete manually. #### Go to Definition Jump to where a function or type is defined: - `F12` or `Cmd+Click` — Go to definition - `Cmd+Shift+F12` — Peek definition (inline) - `Shift+F12` — Find all references ### Configuration Configure the extension in VS Code settings: ```json { "hot.lsp.enabled": true, "hot.lsp.commandPath": "/usr/local/bin/hot", "hot.lsp.extraArgs": [] } ``` | Setting | Default | Description | |---------|---------|-------------| | `hot.lsp.enabled` | `true` | Enable the Language Server (requires Hot CLI) | | `hot.lsp.commandPath` | `hot` | Path to Hot CLI executable | | `hot.lsp.extraArgs` | `[]` | Additional LSP server arguments | ## Language Server Protocol The Hot LSP can be used with any editor that supports LSP. ### Starting the LSP Server ```bash hot lsp ``` The server communicates over stdin/stdout using JSON-RPC. ### Neovim Setup Add to your Neovim configuration: ```lua local lspconfig = require('lspconfig') local configs = require('lspconfig.configs') if not configs.hot then configs.hot = { default_config = { cmd = { 'hot', 'lsp' }, filetypes = { 'hot' }, root_dir = lspconfig.util.root_pattern('hot.hot', '.git'), }, } end lspconfig.hot.setup{} ``` ### Emacs Setup First, define a major mode for `.hot` files: ```elisp (define-derived-mode hot-mode prog-mode "Hot" "Major mode for Hot language files.") (add-to-list 'auto-mode-alist '("\\.hot\\'" . hot-mode)) ``` Then configure `lsp-mode`: ```elisp (use-package lsp-mode :hook (hot-mode . lsp) :config (add-to-list 'lsp-language-id-configuration '(hot-mode . "hot")) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection '("hot" "lsp")) :major-modes '(hot-mode) :server-id 'hot-lsp))) ``` ### Supported Capabilities | Capability | Supported | |------------|-----------| | `textDocument/completion` | ✅ | | `textDocument/hover` | ✅ | | `textDocument/definition` | ✅ | | `textDocument/references` | ✅ | | `textDocument/formatting` | ✅ | | `textDocument/publishDiagnostics` | ✅ | | `textDocument/signatureHelp` | ✅ | | `textDocument/rename` | ✅ | | `workspace/symbol` | ✅ | ## Commands The VS Code extension provides these commands: | Command | Description | |---------|-------------| | `Hot: Start Analyzer` | Start the LSP server | | `Hot: Stop Analyzer` | Stop the LSP server | | `Hot: Restart Analyzer` | Restart the LSP server | | `Hot: Show Logs` | Open the output channel | | `Hot: Create AI Hints` | Generate AI assistant hints | ## REPL Integration The Hot REPL can be used alongside your editor for interactive development: ```bash hot repl ``` Features: - Tab completion - History (up/down arrows) - Multi-line input - Pretty-printed output