--- url: /hclapi/cli/hclapi-openapi.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi openapi ## NAME openapi - Export the compiled OpenAPI 3.1 specification for your manifests. ### SYNOPSIS openapi ``` [--config|-c|--manifests|-m]=[value] [--format|-f]=[value] [--output|-o]=[value] [--pretty] ``` **Usage**: ``` openapi [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] ``` ### GLOBAL OPTIONS **--config, -c, --manifests, -m**="": Path to .hcl file or directory containing manifests. (default: ".") **--format, -f**="": Output format: json or yaml. (default: "json") **--output, -o**="": Path to output file (defaults to stdout). **--pretty**: Pretty-print JSON output. --- url: /hclapi/cli/hclapi-serve.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi serve ## NAME serve - Start the hclapi HTTP server. ### SYNOPSIS serve ``` [--config|-c|--manifests|-m]=[value] [--host|-h]=[value] [--port|-p]=[value] [--verbose|-v] ``` **Usage**: ``` serve [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] ``` ### GLOBAL OPTIONS **--config, -c, --manifests, -m**="": Path to .hcl file, or directory containing manifests. (default: ".") **--host, -h**="": Host address to bind the server (overrides manifest). **--port, -p**="": Port to bind the server (overrides manifest). (default: 0) **--verbose, -v**: Enable verbose debug logging. --- url: /hclapi/cli/hclapi-version.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi version ## NAME version - Show detailed version information. ### SYNOPSIS version **Usage**: ``` version [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] ``` --- url: /hclapi/cli/hclapi.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi ## NAME hclapi - Declarative API runtime that turns HCL manifests into structured HTTP services. ### SYNOPSIS hclapi **Usage**: ``` hclapi [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] ``` ### COMMANDS #### serve Start the hclapi HTTP server. **--config, -c, --manifests, -m**="": Path to .hcl file, or directory containing manifests. (default: ".") **--host, -h**="": Host address to bind the server (overrides manifest). **--port, -p**="": Port to bind the server (overrides manifest). (default: 0) **--verbose, -v**: Enable verbose debug logging. #### openapi Export the compiled OpenAPI 3.1 specification for your manifests. **--config, -c, --manifests, -m**="": Path to .hcl file or directory containing manifests. (default: ".") **--format, -f**="": Output format: json or yaml. (default: "json") **--output, -o**="": Path to output file (defaults to stdout). **--pretty**: Pretty-print JSON output. #### version, v Show detailed version information. --- url: /hclapi/docs/concepts/context.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Execution context `ctx` is the request-scoped object every step reads from. It holds the incoming request and the accumulated output of prior steps. ```text ctx ├── timestamp_epoch : int ├── request │ ├── method : string │ ├── path : map[string]string │ ├── query : map[string]string │ ├── headers : map[string]string │ └── body : any └── steps ├── │ ├── rows : list(map) │ ├── row : map (or null) │ └── rows_affected : int ├── │ └── value : any ├── │ └── result : any └── └── result : any ``` ## Request The request object is populated at ingress and remains immutable for the life of the request. | Field | Type | Example | | :-------------------- | :------------------ | :---------------------------------- | | `ctx.request.method` | `string` | `"POST"` | | `ctx.request.path` | `map[string]string` | `ctx.request.path.id` | | `ctx.request.query` | `map[string]string` | `ctx.request.query.page` | | `ctx.request.headers` | `map[string]string` | `ctx.request.headers.authorization` | | `ctx.request.body` | `any` | `ctx.request.body.email` | Path parameters are bound from route templates. Catch-all parameters such as `{filepath...}` bind the remaining path. Header keys are lowercased at ingress. ## Steps | Step type | Exported fields | Description | | :------------- | :----------------------------- | :------------------------------------------- | | **`sql`** | `rows`, `row`, `rows_affected` | Query results and affected-row count | | **`redis`** | `value` | Retrieved cache value or command result | | **`starlark`** | `result` | Value returned by `execute(ctx)` | | **`go`** | `result` | Value returned by the registered Go function | ## Access from HCL and Starlark In HCL expressions, `steps` is available as a root-level shorthand for `ctx.steps`. In Starlark scripts, values are accessed beneath `ctx` using dictionary syntax. | Value | HCL | Starlark | | :-------------- | :---------------------------------- | :----------------------------------------------- | | Path parameter | `ctx.request.path.id` | `ctx.request.path["id"]` | | Query parameter | `ctx.request.query.filter` | `ctx.request.query.get("filter")` | | Header | `ctx.request.headers.authorization` | `ctx.request.headers.get("authorization")` | | Body field | `ctx.request.body.name` | `ctx.request.body["name"]` | | SQL record | `steps.lookup.row.user_id` | `ctx.steps.lookup.get("row", {}).get("user_id")` | | SQL list | `steps.list_users.rows` | `ctx.steps.list_users["rows"]` | | Redis value | `steps.cache_lookup.value` | `ctx.steps.cache_lookup.get("value")` | | Step result | `steps.compute.result` | `ctx.steps.compute["result"]` | --- url: /hclapi/docs/concepts/errors.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Errors `hclapi` returns [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details for every error, whether raised at ingress, during schema validation, or during pipeline execution. ```json { "type": "urn:hclapi:error:bad-request", "title": "Invalid Request Payload", "status": 400, "detail": "invalid JSON payload: syntax error at line 1, column 9", "instance": "/api/v1/transform", "step": "ingress" } ``` | Field | Type | Description | | :--------------- | :------- | :--------------------------------------------------------- | | `type` | `string` | URI reference identifying the problem type. | | `title` | `string` | Short human-readable summary of the problem type. | | `status` | `int` | HTTP status code. | | `detail` | `string` | Human-readable explanation specific to this occurrence. | | `instance` | `string` | Request URL path that generated the error. | | `step` | `string` | Pipeline step where the failure occurred, if applicable. | | `invalid_params` | `list` | Field-level schema validation errors (RFC 9457 extension). | ## Returning custom errors from Go steps Native `go` steps can return a `hclapi.Problem` to halt the pipeline and emit a specific HTTP status code directly to the client. ### `step.Problem` The fastest way to return an error is using the `step.Problem` helper. The engine automatically binds the step's name, derives the standard title (`http.StatusText`), and builds the canonical type URN: ```go engine.RegisterStep("auth.verify_key", func(ctx context.Context, step *hclapi.Step) (any, error) { apiKey := step.Request.Header("X-API-Key") if apiKey == "" { // Automatically emits HTTP 401 Unauthorized with canonical URN return nil, step.Problem(http.StatusUnauthorized, "Missing or invalid 'X-API-Key' header") } return map[string]any{"authenticated": true}, nil }) ``` ### `hclapi.NewProblem` If you are outside a step or prefer a package-level function: ```go return nil, hclapi.NewProblem(http.StatusNotFound, "Customer record not found") ``` ### 3. Bare struct literal with auto-derivation You can return a `hclapi.Problem` struct literal with only `Status` and `Detail`. The engine automatically infers `Title`, `Type`, `Step`, and `Instance`: ```go return nil, hclapi.Problem{ Status: http.StatusForbidden, Detail: "User does not have permission to delete this project", } ``` ## RFC 9457 Extension Members [RFC 9457 Section 3.2](https://www.rfc-editor.org/info/rfc9457/#section-3.2) allows problem details to be extended with custom members. Any key-value pairs placed in `Extensions` are automatically flattened into the root JSON object: ```go engine.RegisterStep("billing.charge", func(ctx context.Context, step *hclapi.Step) (any, error) { p := step.Problem(http.StatusPaymentRequired, "Insufficient account balance") p.Extensions = map[string]any{ "error_code": "CARD_DECLINED", "current_balance": 14.50, "required_amount": 50.00, "currency": "USD", } return nil, p }) ``` #### Serialized output to the client: ```json { "type": "urn:hclapi:error:payment-required", "title": "Payment Required", "status": 402, "detail": "Insufficient account balance", "instance": "/api/v1/checkout", "step": "billing.charge", "error_code": "CARD_DECLINED", "current_balance": 14.5, "required_amount": 50, "currency": "USD" } ``` ## Overriding error documentation URLs It's possible to override the default `urn:hclapi:error:` prefix by configuring `problem.type_prefix` in the `server {}` block: ```hcl server { problem { type_prefix = "https://docs.mycompany.com/errors/" } } ``` This transforms `urn:hclapi:error:payment-required` into `https://docs.mycompany.com/errors/payment-required`. --- url: /hclapi/docs/concepts/expressions.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Expressions Request-time HCL expressions for context access, conditions, interpolation, argument mapping, and response construction. HCL attributes support expressions evaluated at request time. Expressions read from [`ctx`](/hclapi/docs/concepts/context.md) to map parameters, branch on conditions, interpolate strings, and construct response bodies. ## Operators | Operator | Meaning | Example | | :------------------- | :----------------- | :-------------------------------------------------------------------------- | | `==`, `!=` | Equality | `steps.lookup.rows_affected == 0` | | `>`, `>=`, `<`, `<=` | Numeric comparison | `steps.inventory.result.count < 5` | | `&&` | Logical AND | `steps.auth.result.valid == true && ctx.request.body.admin == true` | | `\|\|` | Logical OR | `ctx.request.query.format == "csv" \|\| ctx.request.query.format == "xlsx"` | | `!` | Logical NOT | `!steps.user.result.is_active` | A field that may be absent is compared against `null`: ```hcl respond { condition = steps.cache_lookup.result != null status = 200 body = steps.cache_lookup.result } ``` ## String interpolation `${...}` substitutes values from `ctx.request` or `steps`. ```hcl redis "session_write" { connection = connection.redis.sessions command = "SET" key = "session:${ctx.request.headers.x_session_id}:user" value = steps.find_user.result.id ttl = "15m" } ``` ## Argument mapping `args` maps context values to parameterized query inputs. Parameters bound through `@param` in a `sql` block are sanitized automatically. ```hcl sql "update_account" { connection = connection.postgres.main query = <<-SQL UPDATE accounts SET name = @name, updated_at = NOW() WHERE id = @id RETURNING id, name, updated_at SQL args = { id = ctx.request.path.id name = steps.sanitize_input.result.clean_name } } ``` ## Response bodies Object and list literals can be constructed inline. ```hcl respond { status = 201 body = { account = steps.create_account.result metadata = { requested_by = ctx.request.headers.authorization created_at = ctx.timestamp_epoch tags = ["api", "v1", ctx.request.query.environment] } } } ``` --- url: /hclapi/docs/concepts/lifecycle.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Request lifecycle hclapi has two phases. Manifests are validated once, at boot. Every request after that runs the same fixed pipeline. | Phase | Trigger | Result | On failure | | :------ | :-------------------- | :------------------------------------------------------ | :----------------------------------------------- | | Boot | `hclapi serve` starts | Manifests parsed, connections opened, routes registered | Process exits with a file and line diagnostic | | Request | HTTP request arrives | Route matched, pipeline runs, response serialized | Request returns RFC 9457 error; server continues | ## Stages ### 1. Ingress The method and path are matched. Headers, query parameters, and body are parsed. ### 2. Context initialization A request-scoped [execution context](/hclapi/docs/concepts/context.md) is created. ### 3. Step execution [Pipeline steps](/hclapi/docs/concepts/pipelines.md) run in order, reading from and appending to the context. ### 4. Response A `respond` step serializes status, headers, and body, and terminates the pipeline. A request that fails schema validation or arrives with unparseable JSON never reaches step execution. --- url: /hclapi/docs/concepts/pipelines.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Pipelines and steps A pipeline is an ordered sequence of steps declared inside an `endpoint` block. Each step performs one unit of work and writes its result into the execution context for subsequent steps to read. | Step | Function | Exported outputs | | :------------------------------------------------- | :------------------------------------------- | :----------------------------- | | [`sql`](/hclapi/docs/steps/sql.md) | Parameterized database queries and mutations | `rows`, `row`, `rows_affected` | | [`redis`](/hclapi/docs/steps/redis.md) | Cache reads, writes, deletions, counters | `value` | | [`starlark`](/hclapi/docs/steps/starlark.md) | Sandboxed data transformation | `result` | | [`go`](/hclapi/docs/steps/go.md) | Invokes a registered native Go function | `result` | | [`transaction`](/hclapi/docs/steps/transaction.md) | Atomic multi-statement SQL execution | Nested SQL outputs | | [`parallel`](/hclapi/docs/steps/parallel.md) | Concurrent branch execution | Nested branch outputs | | [`respond`](/hclapi/docs/steps/respond.md) | Terminates the pipeline | None | ## Step execution rules ### Unique step labels Every step except `respond` and `parallel` requires a unique name. The label defines the namespace under `steps.`. ### Read-only context history A step may read outputs written by prior steps, but cannot modify prior outputs or mutate `ctx.request`. ### Fail-fast errors An unhandled error in any step immediately halts the pipeline and triggers the error handler. ### Early termination Execution stops at the first `respond` whose condition evaluates to `true`, or at the first unconditional `respond`. --- url: /hclapi/docs/index.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi hclapi is a backend engine distributed as a single binary. It turns HashiCorp Configuration Language (HCL) manifests into HTTP APIs, combining data access, business logic, validation, and API definitions in a single declarative configuration, with built-in OpenAPI generation. :::info Request-time manifests Manifests are read and executed at request time. hclapi does not generate or compile Go source. ::: ## Getting started ### [Installation](/hclapi/installation.md) - [Precompiled binaries](/hclapi/installation.md#precompiled-binaries) - [Build from source](/hclapi/installation.md#build-from-source) ### [Quickstart](/hclapi/quickstart.md) - [Your first API](/hclapi/quickstart.md#your-first-api) ## Documentation ### [Concepts](/hclapi/concepts/lifecycle.md) - [Request lifecycle](/hclapi/concepts/lifecycle.md) - [Execution context](/hclapi/concepts/context.md) - [Pipelines and steps](/hclapi/concepts/pipelines.md) - [Expressions](/hclapi/concepts/expressions.md) - [Errors](/hclapi/concepts/errors.md) ### [Manifest](/hclapi/manifest/structure.md) - [Files and merging](/hclapi/manifest/structure.md) - [server](/hclapi/manifest/server.md) - [connection](/hclapi/manifest/connections.md) - [schema](/hclapi/manifest/schemas.md) - [endpoint](/hclapi/manifest/endpoints.md) - [Scalar types](/hclapi/manifest/types.md) ### [Pipeline steps](/hclapi/steps/sql.md) - [sql](/hclapi/steps/sql.md) - [starlark](/hclapi/steps/starlark.md) - [redis](/hclapi/steps/redis.md) - [transaction](/hclapi/steps/transaction.md) - [parallel](/hclapi/steps/parallel.md) - [go](/hclapi/steps/go.md) - [respond](/hclapi/steps/respond.md) ### [Guides](/hclapi/guides/go.md) - [Go integration](/hclapi/guides/go.md#go-integration) ### [Patterns](/hclapi/patterns.md) - [404 on a missing record](/hclapi/patterns.md#404-on-a-missing-record) - [Cache aside](/hclapi/patterns.md#cache-aside) - [Transactional writes](/hclapi/patterns.md#transactional-writes) - [Parallel aggregation](/hclapi/patterns.md#parallel-aggregation) ### [CLI reference](/hclapi/cli/hclapi.md) - [hclapi](/hclapi/cli/hclapi.md) - [hclapi serve](/hclapi/cli/hclapi-serve.md) - [hclapi version](/hclapi/cli/hclapi-version.md) ## Machine-readable documentation ### [llms.txt](/hclapi/llms.txt.md) ### [llms-full.txt](/hclapi/llms-full.txt.md) ## How hclapi works An endpoint consists of an HTTP route, optional request validation, and an ordered pipeline of steps. 1. A request is matched against an `endpoint`. 2. Path, query, header, and body data are validated when a request schema is defined. 3. Pipeline steps execute in order and write their outputs into the request context. 4. A `respond` step terminates the pipeline and writes the HTTP response. See [Request lifecycle](/hclapi/docs/concepts/lifecycle.md) for the complete execution model and [Pipelines and steps](/hclapi/docs/concepts/pipelines.md) for pipeline execution rules. ## What hclapi is for hclapi is intended for small HTTP APIs where the API layer is mostly a thin interface over existing data. It keeps endpoint definitions, queries, validation, and request processing close together in the manifest. hclapi does not replace a general-purpose backend framework. Services that require substantial application logic, complex workflows, long-running state, identity and authentication, schema management, or multiple cooperating services may be better implemented in application code. ## Source Source and issue tracker: [Source Code](https://github.com/ju4n97/hclapi) --- url: /hclapi/docs/installation.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Installation `hclapi` is distributed as a single, statically compiled binary with zero runtime dependencies. ## Quick install ### Linux and macOS Download and install the latest binary matching your operating system and CPU architecture into `/usr/local/bin`: ```bash curl -fsSL https://raw.githubusercontent.com/ju4n97/hclapi/main/scripts/install.sh | bash ``` ### Windows (PowerShell) Download and install the latest binary for Windows: ```powershell irm https://raw.githubusercontent.com/ju4n97/hclapi/main/scripts/install.ps1 | iex ``` ## Linux package managers Direct package installations include the `hclapi` binary in `/usr/bin` and system man pages in `/usr/share/man/man1/hclapi.1`. ### Debian / Ubuntu (`.deb`) Download the `.deb` package from the [releases page](https://github.com/ju4n97/hclapi/releases/latest) and install via `dpkg`: ```bash # Example for x86_64 / amd64 curl -fsSLO https://github.com/ju4n97/hclapi/releases/latest/download/hclapi_0.1.0_linux_amd64.deb sudo dpkg -i hclapi_0.1.0_linux_amd64.deb ``` ### Fedora / RHEL / Rocky Linux (`.rpm`) Download the `.rpm` package from the [releases page](https://github.com/ju4n97/hclapi/releases/latest) and install via `rpm`: ```bash # Example for x86_64 / amd64 curl -fsSLO https://github.com/ju4n97/hclapi/releases/latest/download/hclapi_0.1.0_linux_amd64.rpm sudo rpm -i hclapi_0.1.0_linux_amd64.rpm ``` ### Arch Linux (`.pkg.tar.zst`) Download the native Arch package from the [releases page](https://github.com/ju4n97/hclapi/releases/latest) and install via `pacman`: ```bash # Example for x86_64 / amd64 curl -fsSLO https://github.com/ju4n97/hclapi/releases/latest/download/hclapi_0.1.0_linux_amd64.pkg.tar.zst sudo pacman -U hclapi_0.1.0_linux_amd64.pkg.tar.zst ``` ### Alpine Linux (`.apk`) ```bash curl -fsSLO https://github.com/ju4n97/hclapi/releases/latest/download/hclapi_0.1.0_linux_amd64.apk apk add --allow-untrusted hclapi_0.1.0_linux_amd64.apk ``` ## Precompiled binaries Download a precompiled archive directly from the [GitHub releases](https://github.com/ju4n97/hclapi/releases/latest) page: | Operating system | Architecture | Archive format | Package format | | :--------------- | :---------------------- | :------------- | :------------------------------------- | | **Linux** | 64-bit (`amd64`) | `.tar.gz` | `.deb`, `.rpm`, `.apk`, `.pkg.tar.zst` | | **Linux** | ARM64 (`arm64`) | `.tar.gz` | `.deb`, `.rpm`, `.apk` | | **macOS** | Apple Silicon (`arm64`) | `.tar.gz` | — | | **macOS** | Intel (`amd64`) | `.tar.gz` | — | | **Windows** | 64-bit (`amd64`) | `.zip` | — | | **Windows** | ARM64 (`arm64`) | `.zip` | — | | **FreeBSD** | 64-bit (`amd64`) | `.tar.gz` | — | ### Manual installation (Linux and macOS) ```bash # 1. Detect platform OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" # 2. Download and extract latest release RELEASE_URL="https://github.com/ju4n97/hclapi/releases/latest/download/hclapi_${OS}_${ARCH}.tar.gz" curl -fsSL "$RELEASE_URL" | tar -xz # 3. Move binary to your system PATH sudo install -m 0755 hclapi /usr/local/bin/hclapi ``` ## Container (Docker and Podman) `hclapi` is published as a minimal, distroless multi-architecture OCI image on GitHub Container Registry (supporting `linux/amd64` and `linux/arm64`). ### Pull the image ```bash # Docker docker pull ghcr.io/ju4n97/hclapi:latest # Podman podman pull ghcr.io/ju4n97/hclapi:latest ``` ### Start the HTTP server Mount your local directory containing `.hcl` manifests into the container and bind port `8080`: ```bash # Docker docker run --rm -p 8080:8080 -v "$(pwd):/app:ro" ghcr.io/ju4n97/hclapi:latest serve -c /app # Podman (includes :z for SELinux volume relabeling) podman run --rm -p 8080:8080 -v "$(pwd):/app:ro,z" ghcr.io/ju4n97/hclapi:latest serve -c /app ``` ### Export OpenAPI specification Generate an OpenAPI 3.1 JSON specification to stdout: ```bash docker run --rm -v "$(pwd):/app:ro" ghcr.io/ju4n97/hclapi:latest openapi -c /app --pretty ``` ## Docker Compose Run `hclapi` alongside PostgreSQL and Valkey in a local development environment: ```yaml title="docker-compose.yaml" services: hclapi: image: ghcr.io/ju4n97/hclapi:latest ports: - "8080:8080" volumes: - .:/app:ro command: ["serve", "-c", "/app", "--host", "0.0.0.0", "--port", "8080"] environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/hclapi_db?sslmode=disable - REDIS_URL=redis://valkey:6379/0 depends_on: postgres: condition: service_healthy valkey: condition: service_started postgres: image: postgres:18-alpine environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=hclapi_db ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 valkey: image: valkey/valkey:9.1.2-alpine ports: - "6379:6379" volumes: pgdata: ``` Start the services: ```bash docker compose up -d ``` ## Build from source ### Using Go install Requires [Go 1.27+](https://go.dev/doc/install) installed: ```bash go install github.com/ju4n97/hclapi/cmd/hclapi@latest ``` _(Ensure `$GOPATH/bin` or `$HOME/go/bin` is in your system `$PATH`)_. ### Building locally from git Requires [Task](https://taskfile.dev) installed: ```bash git clone https://github.com/ju4n97/hclapi.git cd hclapi task build # Binary is generated at bin/hclapi ``` ## Verification Check that `hclapi` is installed: ```bash hclapi version ``` Display CLI usage instructions: ```bash hclapi --help ``` ## Verify artifact integrity (optional) All release assets include SHA-256 digests in `checksums.txt`: ```bash # Download checksum file curl -fsSLO https://github.com/ju4n97/hclapi/releases/latest/download/checksums.txt # Verify checksums sha256sum --ignore-missing --check checksums.txt # (On macOS: shasum -a 256 --ignore-missing -c checksums.txt) ``` ## Shell autocompletion `hclapi` supports dynamic command completion for subcommands and flags. ### Bash Add dynamic completion to your `~/.bashrc`: ```bash echo 'complete -o default -C hclapi hclapi' >> ~/.bashrc source ~/.bashrc ``` ### Zsh Add the following to your `~/.zshrc`: ```zsh autoload -Uz +X compinit && compinit autoload -Uz +X bashcompinit && bashcompinit complete -o default -C hclapi hclapi ``` ### Fish Add the following to `~/.config/fish/completions/hclapi.fish`: ```fish complete -c hclapi -f -a '(hclapi --generate-shell-completion (commandline -cop))' ``` ## Man pages Manual pages provide complete offline reference documentation directly in your terminal. :::info Linux package installations like `.deb`, `.rpm`, and `.pkg.tar.zst` install man pages automatically. ::: ### Manual installation (Linux and macOS) If you installed `hclapi` via precompiled binary or `go install`, you can install the man page manually: ```bash # 1. Create the system man directory sudo mkdir -p /usr/local/share/man/man1 # 2. Download the manual page sudo curl -fsSL https://raw.githubusercontent.com/ju4n97/hclapi/main/man/hclapi.1 \ -o /usr/local/share/man/man1/hclapi.1 # 3. Update the system man database (optional) sudo mandb 2>/dev/null || true ``` ### View manual ```bash man hclapi ``` --- url: /hclapi/docs/patterns.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Patterns Recurring pipeline patterns referenced across endpoint configurations. ## 404 on a missing record Query a single resource, verify `rows_affected`, and return a standard `problem()` payload. ```hcl endpoint "GET /api/v1/users/{id}" { pipeline { sql "find_user" { connection = connection.postgres.main query = "SELECT id, name, email FROM users WHERE id = @id" args = { id = ctx.request.path.id } } respond { condition = steps.find_user.rows_affected == 0 status = 404 body = problem(404, "User not found") } respond { status = 200 body = steps.find_user.row } } } ``` ## Cache aside Read the cache, respond immediately on a hit, fall back to the database on a miss, and store the result back into cache. ```hcl endpoint "GET /api/v1/products/{sku}" { pipeline { redis "cache_lookup" { connection = connection.redis.cache command = "GET" key = "cache:product:${ctx.request.path.sku}" } respond { condition = steps.cache_lookup.value != null status = 200 headers = { "X-Cache" = "HIT" } body = json_decode(steps.cache_lookup.value) } sql "db_query" { connection = connection.postgres.main query = "SELECT id, sku, name, price_cents, inventory FROM products WHERE sku = @sku" args = { sku = ctx.request.path.sku } } respond { condition = steps.db_query.rows_affected == 0 status = 404 body = { error = "Product not found" } } redis "cache_write" { connection = connection.redis.cache command = "SET" key = "cache:product:${ctx.request.path.sku}" value = json_encode(steps.db_query.row) ttl = "30m" } respond { status = 200 headers = { "X-Cache" = "MISS" } body = steps.db_query.row } } } ``` ## Transactional writes Two inserts execute atomically inside a transaction, with constraint collisions converted to `409 Conflict`. ```hcl endpoint "POST /api/v1/onboard" { pipeline { starlark "normalize" { source = <<-STARLARK def execute(ctx): email = ctx.request.body.get("email", "").strip().lower() return { "email": email, "full_name": ctx.request.body.get("full_name", "").strip(), "slug": email.split("@")[0] } STARLARK } transaction "provision" { connection = connection.postgres.main sql "insert_user" { query = <<-SQL INSERT INTO users (email, full_name) VALUES (@email, @full_name) RETURNING id, email, full_name, created_at SQL args = { email = steps.normalize.result.email full_name = steps.normalize.result.full_name } catch "23505" { status = 409 body = { error = "A user with this email address already exists" } } } sql "insert_workspace" { query = <<-SQL INSERT INTO workspaces (owner_id, slug, plan) VALUES (@owner_id, @slug, 'free') RETURNING id, slug, plan SQL args = { owner_id = steps.insert_user.row.id slug = steps.normalize.result.slug } catch "23505" { status = 409 body = { error = "Workspace slug collision, please select a custom identifier" } } } } respond { status = 201 body = { user = steps.insert_user.row workspace = steps.insert_workspace.row } } } } ``` ## Parallel aggregation Multiple independent queries execute concurrently and are joined into one response payload. ```hcl endpoint "GET /api/v1/accounts/{id}/overview" { pipeline { parallel { sql "fetch_account" { connection = connection.postgres.main query = "SELECT id, name, tier FROM accounts WHERE id = @id" args = { id = ctx.request.path.id } } sql "fetch_invoices" { connection = connection.postgres.main query = <<-SQL SELECT id, amount_cents, status, issued_at FROM invoices WHERE account_id = @id ORDER BY issued_at DESC LIMIT 5 SQL args = { id = ctx.request.path.id } } sql "fetch_audit" { connection = connection.postgres.main query = <<-SQL SELECT id, action, created_at FROM audit_logs WHERE account_id = @id ORDER BY created_at DESC LIMIT 10 SQL args = { id = ctx.request.path.id } } } respond { condition = steps.fetch_account.rows_affected == 0 status = 404 body = { error = "Account not found" } } respond { status = 200 body = { account = steps.fetch_account.row recent_invoices = steps.fetch_invoices.rows recent_events = steps.fetch_audit.rows } } } } ``` --- url: /hclapi/docs/quickstart.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Quickstart This section creates a manifest, starts the server, and calls two endpoints. ### Create a manifest ```sh mkdir hclapi-quickstart && cd hclapi-quickstart touch main.hcl ``` ```hcl server { host = "127.0.0.1" port = 8080 } endpoint "GET /api/v1/health" { pipeline { respond { status = 200 body = { status = "healthy" timestamp = ctx.timestamp_epoch } } } } endpoint "POST /api/v1/transform" { pipeline { starlark "sanitize" { source = <<-STARLARK def execute(ctx): prefix = ctx.request.body.get("prefix", "item") tags = ctx.request.body.get("tags", []) cleaned = [prefix + ":" + t.strip().lower() for t in tags if len(t.strip()) > 0] return {"count": len(cleaned), "tags": cleaned} STARLARK } respond { status = 200 body = steps.sanitize.result } } } ``` `GET /api/v1/health` responds unconditionally. `POST /api/v1/transform` runs a [`starlark`](/hclapi/docs/steps/starlark.md) step and returns its result. See [Pipelines and steps](/hclapi/docs/concepts/pipelines.md) for how steps pass data to each other. ### Start the server ```sh hclapi serve -c . ``` The server binds to `127.0.0.1:8080`, as declared in the `server` block. Host and port can be overridden without editing the manifest; see [CLI reference](/hclapi/cli/hclapi.md). ```sh hclapi serve -c . --port 9000 --host 0.0.0.0 ``` ### Call the endpoints ```sh curl -i http://localhost:8080/api/v1/health ``` ```http HTTP/1.1 200 OK Content-Type: application/json {"status": "healthy", "timestamp": 1771968000} ``` ```sh curl -i -X POST http://localhost:8080/api/v1/transform \ -H "Content-Type: application/json" \ -d '{"prefix": "env", "tags": [" PROD ","web", "", " US-EAST "]}' ``` ```http HTTP/1.1 200 OK Content-Type: application/json {"count": 3, "tags": ["env:prod", "env:web", "env:us-east"]} ``` Malformed JSON is rejected before the pipeline runs. ```sh curl -i -X POST http://localhost:8080/api/v1/transform \ -d '{"tags": ["invalid" "json"]}' ``` ```http HTTP/1.1 400 Bad Request Content-Type: application/problem+json { "type": "urn:hclapi:error:bad-request", "title": "Invalid Request Payload", "status": 400, "detail": "invalid JSON payload: invalid character '\"' after array element", "instance": "/api/v1/transform" } ``` This is the standard error format used by hclapi. See [Errors](/hclapi/docs/concepts/errors.md). --- url: /hclapi/docs/why.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Why hclapi hclapi is for building small HTTP APIs over existing databases without writing a separate backend for each one. An API is defined in a manifest, with queries and processing steps kept close to the endpoint that uses them. The result is a small service that is easy to read, change, and deploy. hclapi is intentionally narrow. It works best when the API is mostly a thin layer over existing data. ## What it provides :::info Readable endpoints A manifest should be understandable without knowing a framework or data access layer. ::: :::info Cheap changes Query and endpoint changes are file changes rather than application-code changes. ::: :::info Safe defaults SQL parameters are bound instead of interpolated, and Starlark cannot access the filesystem or network. ::: :::info Small surface area The system stays simple enough to understand and maintain. ::: ## Where it does not fit hclapi does not replace a general-purpose backend framework. Use application code when the service needs substantial business logic, complex workflows, or long-running state. Schema management, identity and authentication, and running multiple services are also outside the scope of hclapi. A `go` step is available when logic cannot reasonably live in SQL or Starlark, but at that point hclapi is primarily providing the API layer around Go code. hclapi is best suited to small, data-oriented APIs where keeping the endpoint definition close to the query is more useful than introducing a larger application stack. --- url: /hclapi/guides/go.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Go integration hclapi embeds into an existing Go application as a library. It mounts native HTTP handlers alongside declarative routes and supports calling back into native Go functions from pipeline steps. ## Embedding `hclapi.Engine` implements the standard `http.Handler` interface. It mounts directly onto any `http.ServeMux`, middleware stack, or third-party router. ### Basic setup ```go package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/ju4n97/hclapi" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) engine, err := hclapi.NewEngine(hclapi.Options{ ConfigPath: "./manifests", StrictTyping: true, Logger: logger, }) if err != nil { logger.Error("failed to initialize hclapi engine", "error", err) os.Exit(1) } mux := http.NewServeMux() // Native custom endpoint mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) // Mount hclapi router mux.Handle("/", engine.Handler()) serverConfig := engine.Server() server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: serverConfig.ReadTimeout.Duration(), WriteTimeout: serverConfig.WriteTimeout.Duration(), IdleTimeout: serverConfig.IdleTimeout.Duration(), } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) go func() { logger.Info("server listening", "addr", server.Addr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("server error", "error", err) os.Exit(1) } }() <-stop ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server.Shutdown(ctx) } ``` ## Registering steps Register a Go function that an HCL `go` step can invoke. ### Signature ```go type StepHandler func(ctx context.Context, step *hclapi.Step) (any, error) err := engine.RegisterStep("namespace.function_name", handler) ``` - `ctx`: Standard Go `context.Context` carrying request cancellation, timeouts, and tracing spans. Pass this directly to outbound HTTP requests, database queries, and cache calls. - `step`: The step execution handle carrying evaluated inputs (`step.Args`), request metadata (`step.Request`), and outputs of prior steps (`step.Steps`). ## Step data access `*hclapi.Step` provides thread-safe access to execution data: | Field / Method | Type / Return | Description | | :----------------------------- | :--------------------- | :------------------------------------------------------------------- | | `ctx` | `context.Context` | Cancellation context passed directly into standard library IO calls | | `step.Args` | `hclapi.Args` | Evaluated arguments defined in the manifest `args = { ... }` block | | `step.Request` | `*hclapi.RequestState` | Ingress HTTP metadata (`Method`, `Path`, `Query`, `Headers`, `Body`) | | `step.Request.Header(key)` | `string` | Case-insensitive header lookup | | `step.Request.PathParam(key)` | `string` | Route path parameter lookup | | `step.Request.QueryParam(key)` | `string` | Query string parameter lookup with optional fallback | | `step.GetStepResult(name)` | `(StepResult, bool)` | Safely retrieve outputs from a prior pipeline step | | `step.TimestampEpoch` | `int64` | Ingress Unix epoch timestamp | | `step.RawRequest` | `*http.Request` | The underlying Go standard library request | ## Working with arguments (`step.Args`) `step.Args` uses generic methods for type coercion, handling HCL integers (`int64`), floating-point values, and dynamic lists: ```go // Explicit typed extraction (returns zero value and false if missing or invalid) latitude, ok := step.Args.Get[float64]("latitude") if !ok { return nil, errors.New("missing or invalid 'latitude' argument") } // Generic type inference with defaults port := step.Args.GetOr("port", 8080) // Inferred as int host := step.Args.GetOr("host", "localhost") // Inferred as string priority := step.Args.GetOr("priority", false) // Inferred as bool timeout := step.Args.GetOr("timeout_s", 2.5) // Inferred as float64 // Typed list extraction (safely coerces HCL dynamic []any slices) tags := step.Args.Slice[string]("tags") ids := step.Args.Slice[int]("user_ids") // Struct binding (unmarshals arguments directly into a Go struct) var config ServiceConfig if err := step.Args.Bind(&config); err != nil { return nil, fmt.Errorf("invalid arguments: %w", err) } ``` ## Example: Outbound HTTP Call ### 1. In your Go application: ```go package main import ( "context" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/ju4n97/hclapi" ) type WeatherResponse struct { Current struct { Temperature float64 `json:"temperature_2m"` Humidity int `json:"relative_humidity_2m"` } `json:"current"` } var httpClient = &http.Client{Timeout: 5 * time.Second} func registerWeatherStep(engine *hclapi.Engine) error { return engine.RegisterStep("services.fetch_weather", func(ctx context.Context, step *hclapi.Step) (any, error) { // Read arguments using type-safe getters lat, okLat := step.Args.Get[float64]("latitude") lon, okLon := step.Args.Get[float64]("longitude") if !okLat || !okLon { return nil, errors.New("missing or invalid 'latitude' or 'longitude' arguments") } url := fmt.Sprintf( "https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f¤t=temperature_2m,relative_humidity_2m", lat, lon, ) // Pass standard ctx directly to outgoing HTTP request for timeout/cancellation propagation req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("external weather API request failed: %w", err) } defer resp.Body.Close() var data WeatherResponse if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, fmt.Errorf("failed to decode weather API response: %w", err) } // Return a map, struct, or primitive. The output is exported under steps..result (steps.weather.result in this example) return map[string]any{ "temperature_c": data.Current.Temperature, "humidity_pct": data.Current.Humidity, "fetched_at": step.TimestampEpoch, }, nil }) } ``` ### 2. In your HCL manifest: ```hcl endpoint "GET /api/v1/locations/{city}/weather" { pipeline { sql "get_coords" { connection = connection.postgres.main query = "SELECT latitude, longitude FROM locations WHERE city = @city" args = { city = ctx.request.path.city } } respond { condition = steps.get_coords.rows_affected == 0 status = 404 body = { error = "Location not found" } } go "weather" { use = "services.fetch_weather" args = { latitude = steps.get_coords.row.latitude longitude = steps.get_coords.row.longitude } } respond { status = 200 body = { city = ctx.request.path.city temperature = steps.weather.result.temperature_c humidity = steps.weather.result.humidity_pct } } } } ``` ## Error handling and custom status codes - **Standard Go errors:** Returning a standard Go error (e.g. `errors.New("db failure")`) automatically returns an HTTP 500 with problem type `urn:hclapi:error:pipeline-execution-failed`. - **Custom HTTP status codes:** Return `step.Problem(status, detail)` or a `hclapi.Problem` struct to emit custom status codes (such as 401, 403, 404, or 429) directly to the client. - **Panics:** Any panic inside a `StepHandler` is automatically recovered by the runtime, logged with the step's name and stack trace, and returned as an HTTP 500. The server process continues running uninterrupted. --- url: /hclapi/index.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hclapi Declare your backend > Turn HCL manifests into production-ready HTTP APIs without the usual backend boilerplate. [Get Started](/docs/quickstart) | [Manifest](/docs/manifest/structure) ## Features - [ **Declarative**](/docs/manifest/structure): Define servers, connections, schemas, endpoints, and backend behavior directly in HCL. - [ **Composable**](/docs/concepts/pipelines): Build request pipelines from SQL, Redis, Go, Starlark, transactions, parallel execution, and more. - [ **Explicit**](/docs/concepts/lifecycle): Keep the entire request lifecycle visible from the incoming HTTP request to the final response. - [ **Database-native**](/docs/steps/sql): Make SQL and Redis operations first-class parts of your API execution pipeline. - [ **Extensible**](/guides/go): Use Go integration and Starlark when declarative configuration is not enough. - [ **Built-in expressions**](/docs/manifest/functions/system/env): Work with strings, collections, encoding, cryptography, math, and system functions directly in your manifests. --- url: /hclapi/openapi/configuration.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # OpenAPI configuration OpenAPI document metadata, target servers, tags, and endpoint handler options are configured using standard HCL blocks. ## Global specification metadata (`server.openapi {}`) Declared inside `server {}` to configure the global document header: ```hcl server { host = "0.0.0.0" port = 8080 openapi { title = "Acme Storefront API" version = "1.2.0" description = <<-MARKDOWN ## Overview Production customer account and order management API service. ### Authentication Most endpoints require a valid API key passed via the `X-API-Key` header. MARKDOWN servers = [ { url = "https://api.example.com/v1" description = "Production cluster" }, { url = "http://localhost:8080" description = "Local development server" } ] tags = [ { name = "users" description = "Customer accounts, authentication, and profiles" }, { name = "orders" description = "Order checkout, cart mutations, and payment history" } ] contact { name = "API Engineering" email = "support@example.com" url = "https://example.com/developers" } license { name = "MIT" url = "https://opensource.org/licenses/MIT" } } } ``` ## Global metadata attributes (`server.openapi`) | Attribute | Type | Default | Description | | :------------ | :------------- | :-------------------- | :------------------------------------------------------------------- | | `title` | `string` | `"API Documentation"` | Human-readable title in OpenAPI `info.title` | | `version` | `string` | `"1.0.0"` | API version in OpenAPI `info.version` | | `description` | `string` | `""` | Multi-line Markdown description in OpenAPI `info.description` | | `servers` | `list(object)` | `[]` | List of deployment servers (`url`, `description`) | | `tags` | `list(object)` | `[]` | List of category tags for operation grouping (`name`, `description`) | | `contact` | `block` | `null` | Contact information (`name`, `email`, `url`) | | `license` | `block` | `null` | Legal license information (`name`, `url`) | ## Endpoint handler attributes (`endpoint.openapi`) Declared inside an `endpoint` block to serve a documentation UI or raw specification: | Attribute | Type | Default | Description | | :-------------- | :------- | :----------- | :--------------------------------------------------------------------- | | `ui` | `string` | `"scalar"` | Built-in UI renderer: `"scalar"`, `"elements"`, `"swagger"`, `"redoc"` | | `format` | `string` | `null` | Raw specification output format: `"json"` or `"yaml"` | | `spec_url` | `string` | Auto-derived | URL path to the OpenAPI specification to load | | `template` | `string` | `""` | Custom HTML template string (heredoc) | | `template_file` | `string` | `""` | Path to custom HTML template file relative to manifest | --- url: /hclapi/openapi/overview.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # OpenAPI overview `hclapi` statically analyzes your manifest Abstract Syntax Tree (AST) at boot time to compile a strict, 100% compliant **OpenAPI 3.1** specification. You don't need to maintain separate YAML files, write code annotations, or run external generators. The HCL manifest is the API contract. Additionally, no hidden routes are mounted behind your back. If an OpenAPI endpoint or interactive documentation portal is served by your application, it's explicitly declared as an `endpoint` block. ## What is automatically derived Because `hclapi` manifests are statically typed, the compiler maps every part of your manifest directly into OpenAPI 3.1: | Manifest element | OpenAPI 3.1 element | | :----------------------------------- | :--------------------------------------------------------------- | | `endpoint "POST /api/v1/users/{id}"` | `paths["/api/v1/users/{id}"]["post"]` | | `description = "..."` | `operation.description` and `operation.summary` | | `{id}` and `{filepath...}` | `parameters` with `in: path`, `required: true` | | `request.query` fields | `parameters` with `in: query` (with types, defaults, and bounds) | | `request.headers` fields | `parameters` with `in: header` (with format constraints) | | `request.body` / `schema` | `requestBody.content["application/json"].schema` | | `respond { status = 200 }` | `responses["200"]` | | `catch "23505" { status = 409 }` | `responses["409"]` (Conflict) | | Schema validation engine | `responses["422"]` (Unprocessable Entity) | | Request body size enforcement | `responses["413"]` (Payload Too Large) | ## Explicit endpoint declaration You control the exact paths, renderers, and access guards for your documentation by declaring explicit `endpoint` blocks: ```hcl # Public interactive documentation portal using Scalar endpoint "GET /docs" { description = "Public interactive API documentation." auth = [] openapi { ui = "scalar" } } # Raw OpenAPI 3.1 JSON specification endpoint "GET /openapi.json" { openapi { format = "json" } } # Raw OpenAPI 3.1 YAML specification endpoint "GET /openapi.yaml" { openapi { format = "yaml" } } # Internal Swagger UI protected by Basic Auth endpoint "GET /admin/swagger" { description = "Internal Swagger UI for engineering team." auth = [auth.basic_admin] openapi { ui = "swagger" } } ``` ## Static CLI export You can export the compiled OpenAPI document directly from your terminal or CI/CD pipeline without starting the HTTP server: ```sh # Export JSON to stdout hclapi openapi -c ./api # Export JSON to file hclapi openapi -c ./api -o ./openapi.json # Export YAML to stdout hclapi openapi -c ./api --format=yaml # Export YAML to file hclapi openapi -c ./api --format=yaml -o ./openapi.yaml ``` The exported specification can be fed directly into an OpenAPI SDK generator or client generator. ## SDK options | Languages | Recommendation | | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | TypeScript / JavaScript | [openapi-typescript](https://github.com/openapi-ts/openapi-typescript), [hey-api](https://github.com/hey-api/openapi-ts), [Orval](https://github.com/orval-labs/orval) | | Python, Go, Java, C# / .NET, PHP, Ruby, Rust | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) | | Go | [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) | | Python | [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator) | | Rust | [Progenitor](https://github.com/oxidecomputer/progenitor) | --- url: /hclapi/openapi/renderers.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Interactive renderers `hclapi` embeds 4 interactive documentation UI renderers directly inside the single binary. Renderers are served by declaring an `endpoint` with an `openapi {}` block. ## 1. Scalar (Default) A modern, fast, and clean interactive API reference featuring a dark/light mode toggle, integrated interactive request client, and code snippet generation in 15+ languages. ```hcl endpoint "GET /docs" { description = "Interactive API reference portal." auth = [] openapi { ui = "scalar" } } ``` ## 2. Stoplight Elements A clean, responsive, Stripe-like documentation UI with a two-column layout, sidebar navigation, and integrated request testing. ```hcl endpoint "GET /docs/elements" { openapi { ui = "elements" } } ``` ## 3. Swagger UI The classic, widely recognized interactive OpenAPI testing interface. ```hcl endpoint "GET /swagger" { openapi { ui = "swagger" } } ``` ## 4. Redoc A 3-column documentation layout designed for deep technical reading. ```hcl endpoint "GET /redoc" { openapi { ui = "redoc" } } ``` ## Custom HTML templates To completely customize your documentation portal, omit `ui` and provide your own HTML template via an inline heredoc or an external file. ### Inline heredoc: ```hcl endpoint "GET /docs" { openapi { template = <<-HTML {{ .Title }} - Developer Hub
HTML } } ``` ### External template file: ```hcl endpoint "GET /docs" { openapi { template_file = "./custom-portal.html" # Resolved relative to the manifest directory } } ``` ## Template variables All HTML templates (built-in and custom) have access to the following data context: | Variable | Description | Example value | | :------------------- | :---------------------------- | :-------------------------- | | `{{ .Title }}` | Configured API title | `"Acme Storefront API"` | | `{{ .Version }}` | Configured API version | `"1.2.0"` | | `{{ .Description }}` | Rendered Markdown description | `"Production API service."` | | `{{ .SpecURL }}` | Route path to the JSON spec | `"/openapi.json"` | | `{{ .SpecYAMLURL }}` | Route path to the YAML spec | `"/openapi.yaml"` | ## Protecting documentation with Basic Auth Since documentation UIs are explicit `endpoint` blocks, you can restrict access to internal API portals using standard authentication guards: ```hcl # 1. Define Basic Auth guard auth "basic_admin" { type = "basic" username = env("ADMIN_USER") password = env("ADMIN_PASS") } # 2. Protect Swagger UI with Basic Auth endpoint "GET /admin/swagger" { description = "Internal Swagger UI for engineering team." auth = [auth.basic_admin] openapi { ui = "swagger" } } ``` --- url: /hclapi/docs/manifest/structure.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Files and merging hclapi builds its runtime tree from a single file, or by walking a directory and merging every manifest it finds into one service definition. ## Recognized files | Pattern | Example | | :------ | :---------------------------- | | `*.hcl` | `main.hcl`, `connections.hcl` | Non-manifest files such as `index.md`, `init.sql`, `.gitignore`, and static assets are ignored during discovery. ## Directory scanning When passed a directory with `hclapi serve -c ./config`, the parser walks the tree recursively. Directories beginning with a dot (`.git`, `.cache`) are skipped. Endpoints, connections, schemas, and server settings found under the tree are merged into one AST. ## Merge rules :::warning Duplicate endpoint detection Endpoints are identified by HTTP method and path. Declaring the same pair in more than one file halts startup with a diagnostic naming both files. ::: Server blocks merge by attribute. If multiple files declare `server {}`, the last evaluated value wins for explicitly set attributes; unset attributes retain defaults. Connections and schemas occupy global namespaces. A connection labeled `connection "postgres" "primary"` in one file is available to endpoints in another file. ## Layouts ### Flat layout ```text title="my-service/" my-service/ ├── main.hcl └── docker-compose.yaml ``` ### Domain-driven layout ```text title="api-service/" api-service/ ├── server.hcl ├── connections.hcl ├── schemas/ │ ├── account.hcl │ └── user.hcl └── routes/ ├── accounts.hcl └── users.hcl ``` ### Versioned layout ```text gateway/ ├── server.hcl ├── schemas/ │ ├── v1.hcl │ └── v2.hcl └── routes/ ├── v1/ └── v2/ ``` `hclapi serve -c ./gateway` merges all versioned routes into the same router. --- url: /hclapi/docs/manifest/server.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # server Declares transport-level settings: listener interface and port binding, connection timeouts, maximum request body size, RFC 9457 problem type resolution, and global OpenAPI metadata. ## Declaration ```hcl server { host = "0.0.0.0" port = 8080 read_timeout = "15s" write_timeout = "30s" idle_timeout = "60s" max_body_size = "25MB" problem { type_prefix = "https://docs.mycompany.com/errors/" } openapi { title = "Acme Storefront API" version = "1.0.0" } } ``` ## Transport attributes All attributes are optional and inherit production-ready defaults. | Attribute | Type | Default | Description | | :-------------- | :--------- | :------------ | :-------------------------------------------------------------------------------- | | `host` | `string` | `"127.0.0.1"` | Network interface to bind. Use `"0.0.0.0"` to listen on all interfaces. | | `port` | `int` | `8080` | TCP port. | | `read_timeout` | `Duration` | `"15s"` | Maximum duration allowed to read the full request headers and body. | | `write_timeout` | `Duration` | `"15s"` | Maximum duration allowed before timing out write operations on the response. | | `idle_timeout` | `Duration` | `"60s"` | Maximum time an idle keep-alive connection remains open. | | `max_body_size` | `ByteSize` | `"10MB"` | Requests with bodies exceeding this limit are immediately rejected with HTTP 413. | ## Child blocks ### `problem` Configures [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) error type resolution for the application. ```hcl server { problem { type_prefix = "https://docs.mycompany.com/errors/" } } ``` | Attribute | Type | Default | Description | | :------------ | :------- | :------ | :------------------------------------------------------- | | `type_prefix` | `string` | `""` | Base type prefix for human-readable error documentation. | #### Problem type URI resolution - **Without `problem.type_prefix` (default):** Emits standard URNs (`urn:hclapi:error:bad-request`). - **With a documentation URL:** `type_prefix = "https://docs.example.com/errors/"` resolves to `https://docs.example.com/errors/bad-request`. - **With a custom URN prefix:** `type_prefix = "urn:acme:error:"` resolves to `urn:acme:error:bad-request`. ### `openapi` Declares top-level metadata for the automatically compiled OpenAPI 3.1 specification (title, description, deployment servers, category tags, contact, and license). ```hcl server { openapi { title = "Acme Storefront API" version = "1.2.0" description = "Customer accounts and order processing API." } } ``` :::tip Full OpenAPI reference All available options are documented in the [OpenAPI configuration guide](/hclapi/openapi/configuration.md). ::: ## Example ```hcl server { host = "0.0.0.0" port = 8080 read_timeout = "30s" write_timeout = "60s" idle_timeout = "120s" max_body_size = "50MB" problem { type_prefix = "https://developer.example.com/api/errors/" } openapi { title = "Production API Gateway" version = "2.0.0" } } ``` --- url: /hclapi/docs/manifest/connections.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # connection Declares access parameters, connection pool sizing, and lifecycle policies for a database, cache, or storage backend. ## Declaration A `connection` block takes two labels: the canonical driver identifier and a unique connection name. ```hcl connection "" "" { url = env("DATABASE_URL") pool { max_open_conns = 25 max_idle_conns = 5 conn_max_lifetime = "30m" idle_timeout = "5m" } } ``` Reference the connection as `connection..`. ## Attributes | Attribute | Type | Required | Description | | :----------- | :------- | :------- | :----------------------------------------- | | Driver label | `string` | yes | Canonical driver identifier | | Name label | `string` | yes | Unique name within the driver namespace | | `url` | `string` | yes | DSN or URI; supports `env(...)` resolution | | `pool` | `block` | no | Connection-pool tuning parameters | ### Pool settings | Attribute | Type | Default | Description | | :------------------ | :--------- | :------ | :---------------------------------------- | | `max_open_conns` | `int` | `25` | Maximum number of open SQL connections | | `max_idle_conns` | `int` | `5` | Maximum idle SQL connections retained | | `conn_max_lifetime` | `Duration` | `"30m"` | Maximum reuse duration | | `idle_timeout` | `Duration` | `"5m"` | Maximum idle duration before eviction | | `size` | `int` | `20` | Pool capacity for cache/key-value drivers | ## Supported drivers | Driver | Category | Typical targets | | :------------ | :----------------- | :--------------------------------------------------- | | `postgres` | Relational SQL | PostgreSQL, Supabase, TimescaleDB, Aurora PostgreSQL | | `sqlite` | Embedded SQL | SQLite, Turso, LibSQL | | `mysql` | Relational SQL | MySQL, MariaDB, PlanetScale, TiDB, Aurora MySQL | | `sqlserver` | Relational SQL | SQL Server, Azure SQL | | `oracle` | Relational SQL | Oracle Database 11g–23ai | | `cockroachdb` | Distributed SQL | CockroachDB | | `clickhouse` | Columnar SQL | ClickHouse Cloud / self-hosted | | `duckdb` | Embedded analytics | DuckDB | | `redis` | Key-value/cache | Redis, Valkey, ElastiCache | | `s3` | Blob storage | S3, R2, MinIO, GCS-compatible endpoints | ## Example: primary and replica pools ```hcl connection "postgres" "primary" { url = env("DATABASE_PRIMARY_URL") pool { max_open_conns = 50 max_idle_conns = 10 conn_max_lifetime = "1h" idle_timeout = "10m" } } connection "postgres" "replica" { url = env("DATABASE_REPLICA_URL") pool { max_open_conns = 100 max_idle_conns = 20 } } ``` ## Reference in a pipeline ```hcl endpoint "GET /api/v1/users/{id}" { pipeline { sql "find_user" { connection = connection.postgres.replica query = "SELECT id, name, email FROM users WHERE id = @id" args = { id = ctx.request.path.id } } redis "cache_user" { connection = connection.redis.cache command = "SET" key = "user:${ctx.request.path.id}" value = json_encode(steps.find_user.row) ttl = "15m" } respond { status = 200 body = steps.find_user.row } } } ``` --- url: /hclapi/docs/manifest/schemas.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # schema Declares structural, type, and semantic validation rules for incoming requests. Validation runs during request ingress before the pipeline begins execution. ## Declaration ```hcl schema "user_create" { field "email" { type = string required = true format = "email" description = "Primary user login and notification email" } field "account_type" { type = string required = true enum = ["individual", "business"] } field "username" { type = string required = true min_length = 3 max_length = 30 pattern = "^[a-zA-Z0-9_-]+$" } field "age" { type = int required = false min = 18 max = 120 } field "role" { type = string default = "member" } field "tags" { type = list(string) required = false min_items = 1 max_items = 10 unique_items = true } } ``` The schema is referenced in endpoint definitions as `schema.`. ## Field attributes & constraints All field constraints map 1:1 to OpenAPI 3.1 / JSON Schema standards: | Attribute | Type | Applicable types | Default | Description | | :------------- | :------- | :----------------------- | :------- | :------------------------------------------------------------------------------------ | | `type` | `type` | All | required | Expected data type (`string`, `int`, `float`, `bool`, `any`, `list(...)`, `map(...)`) | | `required` | `bool` | All | `false` | If `true`, rejects the request with HTTP 422 if the field is missing | | `default` | `any` | Optional fields | `null` | Fallback value injected automatically if field is absent | | `description` | `string` | All | `null` | Human-readable explanation exported to OpenAPI documentation | | `enum` | `list` | `string`, `int`, `float` | `null` | Restricts allowed values to an explicit list | | `format` | `string` | `string` | `null` | Standard OpenAPI format constraint (see below) | | `pattern` | `string` | `string` | `null` | Regular expression pattern the string must match | | `min_length` | `int` | `string` | `null` | Minimum string character length | | `max_length` | `int` | `string` | `null` | Maximum string character length | | `min` | `number` | `int`, `float` | `null` | Minimum numeric value (inclusive) | | `max` | `number` | `int`, `float` | `null` | Maximum numeric value (inclusive) | | `min_items` | `int` | `list` | `null` | Minimum number of items in a list | | `max_items` | `int` | `list` | `null` | Maximum number of items in a list | | `unique_items` | `bool` | `list` | `false` | If `true`, requires all elements in the list to be unique | ## Built-in format validators | Format | Description | Example valid value | | :------------ | :-------------------------------- | :--------------------------------------- | | `"email"` | RFC 5322 email address | `"jane@example.com"` | | `"uuid"` | UUID v4 / v7 identifier | `"f47ac10b-58cc-4372-a567-0e02b2c3d479"` | | `"uri"` | RFC 3986 absolute URI | `"https://api.example.com/callback"` | | `"date-time"` | RFC 3339 / ISO 8601 timestamp | `"2026-08-31T20:00:00Z"` | | `"date"` | RFC 3339 full-date (`YYYY-MM-DD`) | `"2026-08-31"` | | `"ipv4"` | Dotted-decimal IPv4 address | `"192.168.1.1"` | | `"ipv6"` | Colon-separated IPv6 address | `"2001:0db8:85a3::8a2e:0370:7334"` | | `"hostname"` | RFC 1123 Hostname / FQDN | `"api.service.internal"` | ## Binding schemas to endpoints ### Named schema reference ```hcl endpoint "POST /api/v1/users" { request { body = schema.user_create } pipeline { sql "insert_user" { connection = connection.postgres.main query = "INSERT INTO users (email, name, role) VALUES (@email, @name, @role) RETURNING id" args = { email = ctx.request.body.email name = ctx.request.body.username role = ctx.request.body.role # Automatically receives default "member" if omitted } } respond { status = 201 body = steps.insert_user.row } } } ``` ### Inline parameter validation (Path, Query, Headers) ```hcl endpoint "GET /api/v1/search" { request { headers { field "x-api-key" { type = string required = true format = "uuid" } } query { field "q" { type = string required = true min_length = 2 } field "limit" { type = int default = 20 min = 1 max = 100 } field "sort" { type = string default = "desc" enum = ["asc", "desc"] } } } pipeline { respond { status = 200 body = { query = ctx.request.query.q limit = ctx.request.query.limit sort = ctx.request.query.sort } } } } ``` :::tip Header case-insensitivity (RFC 9110) HTTP field names are case-insensitive. You can declare header fields in any casing: ```hcl headers { field "Authorization" { type = string required = true } field "X-Trace-Sampled" { type = bool default = true } } ``` ::: ## Validation failure responses (HTTP 422) When an incoming payload violates schema constraints, execution halts immediately before the pipeline runs. The engine returns an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details object detailing every failed constraint: ```http HTTP/1.1 422 Unprocessable Entity Content-Type: application/problem+json { "type": "urn:hclapi:error:validation-error", "title": "Unprocessable Entity", "status": 422, "detail": "Request payload failed schema validation constraints", "instance": "/api/v1/users", "invalid_params": [ { "name": "email", "reason": "must be a valid email format" }, { "name": "account_type", "reason": "must be one of: [\"individual\", \"business\"]" }, { "name": "username", "reason": "length must be at least 3 characters" }, { "name": "age", "reason": "must be greater than or equal to 18" } ] } ``` --- url: /hclapi/docs/manifest/endpoints.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # endpoint Binds an HTTP method and path to a request schema and a pipeline. ## Declaration ```hcl endpoint "GET /api/v1/accounts/{id}" { description = "Fetches account details by identifier" request { path { field "id" { type = int required = true } } } pipeline { sql "find_account" { connection = connection.postgres.main query = "SELECT id, name, tier FROM accounts WHERE id = @id" args = { id = ctx.request.path.id } } respond { condition = steps.find_account.rows_affected == 0 status = 404 body = { error = "Account not found" } } respond { status = 200 body = steps.find_account.result } } } ``` ## Attributes | Attribute | Type | Required | Description | | :----------------------------- | :------- | :------- | :------------------------------------------------------------- | | Route label (`"METHOD /path"`) | `string` | yes | HTTP method and path pattern | | `description` | `string` | no | Used in logs and generated API documentation | | `auth` | `list` | no | Route-level authentication guards; `[]` marks the route public | | `request` | `block` | no | Validation for path, query, headers, and body | | `pipeline` | `block` | yes | Steps that handle the request | Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD`. ## Path templates A single segment binds to `ctx.request.path.`. ```hcl endpoint "GET /api/v1/organizations/{org_id}/members/{member_id}" { pipeline { respond { status = 200 body = { org = ctx.request.path.org_id member = ctx.request.path.member_id } } } } ``` A trailing `...` matches all remaining segments: ```hcl endpoint "GET /static/{filepath...}" { pipeline { respond { status = 200 body = { path = ctx.request.path.filepath } } } } ``` ## Request validation The `request` block validates path, query, headers, and body independently. | Sub-block | Target | Validates | | :-------- | :-------------------- | :--------------------------- | | `path` | `ctx.request.path` | Route parameter types | | `query` | `ctx.request.query` | Query string and defaults | | `headers` | `ctx.request.headers` | Required headers and formats | | `body` | `ctx.request.body` | JSON body against a schema | ## Authentication overrides An endpoint inherits global authentication guards unless it opts out explicitly. ```hcl endpoint "GET /health/live" { description = "Bypassed by load balancers" auth = [] pipeline { respond { status = 200 body = { status = "healthy", timestamp = ctx.timestamp_epoch } } } } ``` --- url: /hclapi/docs/manifest/types.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # Types and schema constraints hclapi features a strongly typed schema and configuration system. Types are verified at boot time during manifest parsing and enforced at request ingress before pipeline execution. ## Schema data types Used inside `schema` and `request` field definitions (`field "" { type = }`): | Signature | JSON representation | Description | Example | | :----------------- | :------------------------------ | :--------------------------------------------------- | :-------------------- | | **`string`** | `"hello"` | Text strings (supports regex `pattern` and `format`) | `type = string` | | **`int`** | `42` | 64-bit signed integer | `type = int` | | **`float`** | `3.14159` | 64-bit floating-point number | `type = float` | | **`bool`** | `true`, `false` | Boolean truth value | `type = bool` | | **`any`** | Any primitive, array, or object | Free-form untyped payload | `type = any` | | **`list()`** | `["admin", "member"]` | Array of uniform elements | `type = list(string)` | | **`map()`** | `{"k1": "v1"}` | String-keyed dictionary with uniform values | `type = map(int)` | ### Examples ```hcl schema "product" { field "sku" { type = string, required = true } field "price" { type = float, required = true, min = 0.01 } field "tags" { type = list(string), default = [] } field "metadata" { type = map(any) } } ``` ## Scalar configuration types hclapi provides specialized scalar types that parse human-readable strings into typed units at startup and fail fast on invalid syntax. ### Duration Backed by Go's `time.Duration`. | Suffix | Unit | Example | | :--------- | :----------- | :-------- | | `ns` | Nanoseconds | `"500ns"` | | `us`, `µs` | Microseconds | `"100µs"` | | `ms` | Milliseconds | `"250ms"` | | `s` | Seconds | `"30s"` | | `m` | Minutes | `"15m"` | | `h` | Hours | `"1h30m"` | Units can be combined (e.g. `"1h30m"`, `"2m45s"`). #### Usage in manifests: ```hcl server { read_timeout = "30s" idle_timeout = "2m" } connection "postgres" "main" { pool { conn_max_lifetime = "1h" idle_timeout = "10m" } } ``` ### ByteSize Backed by a 64-bit integer representing byte quantities. Both decimal (1,000-based) and binary (1,024-based) units are accepted case-insensitively. | Suffix | Standard | Bytes | | :-------- | :------- | :------------------ | | `B` | Byte | `1` | | `KB`, `K` | Kilobyte | `1,000` | | `KiB` | Kibibyte | `1,024` | | `MB`, `M` | Megabyte | `1,000,000` | | `MiB` | Mebibyte | `1,048,576` | | `GB`, `G` | Gigabyte | `1,000,000,000` | | `GiB` | Gibibyte | `1,073,741,824` | | `TB`, `T` | Terabyte | `1,000,000,000,000` | | `TiB` | Tebibyte | `1,099,511,627,776` | Fractional quantities and raw integer bytes are both supported: ```hcl server { max_body_size = "25MB" # 25,000,000 bytes max_body_size = "2.5MiB" # 2,621,440 bytes max_body_size = "1048576" # 1,048,576 raw integer bytes } ``` ## Compile-time diagnostics Invalid type or scalar formats fail fast during `hclapi serve` boot: ```hcl server { read_timeout = "100years" } ``` ```text error: server: invalid read_timeout: invalid duration "100years": time: unknown unit "years" in duration "100years" ``` ```hcl server { max_body_size = "10XB" } ``` ```text error: server: invalid max_body_size: invalid byte size "10XB": unknown unit "XB" ``` --- url: /hclapi/docs/manifest/functions/system/env.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # env Reads the value of an environment variable from the host operating system. ## Signature ```hcl env(name: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :------------------------ | | name | string | yes | Environment variable name | ## Return value Returns the documented result for the function signature. ## Example ```hcl connection "postgres" "main" { url = env("DATABASE_URL") } ``` An unset variable returns an empty string `""`. --- url: /hclapi/docs/manifest/functions/system/uuid.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # uuid / uuid\_v4 Generates a cryptographically secure random UUID version 4 string. `uuid()` is an alias to `uuid_v4()`. ## Signature ```hcl uuid() -> string uuid_v4() -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :------------ | | None | — | — | No parameters | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 201 headers = { "X-Request-ID" = uuid() } body = { session_id = uuid_v4(), status = "created" } } ``` `uuid()` is an alias for `uuid_v4()`. --- url: /hclapi/docs/manifest/functions/system/uuid_v7.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # uuid\_v7 Generates a time-ordered UUID version 7 string (RFC 9562). UUID v7 combines a Unix millisecond timestamp with random entropy, preventing B-Tree index fragmentation and page thrashing in database primary keys. ## Signature ```hcl uuid_v7() -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :------------ | | None | — | — | No parameters | ## Return value Returns the documented result for the function signature. ## Example ```hcl sql "create_order" { connection = connection.postgres.main query = "INSERT INTO orders (id, user_id, amount) VALUES (@id, @user_id, @amount) RETURNING id" args = { id = uuid_v7(), user_id = ctx.request.body.user_id, amount = ctx.request.body.amount } } ``` --- url: /hclapi/docs/manifest/functions/system/now.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # now Returns the current system timestamp in UTC formatted according to RFC 3339. ## Signature ```hcl now() -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :------------ | | None | — | — | No parameters | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { status = "ok", received_at = now(), epoch = ctx.timestamp_epoch } } ``` --- url: /hclapi/docs/manifest/functions/system/problem.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # problem Constructs an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) compliant Problem Details error payload. Supports both positional shorthand for common errors and map syntax for structured validation errors and custom metadata extensions. ## Signatures ```hcl # 1. Positional shorthand (simple errors) problem(status: int, detail: string, custom_type: string...) -> map # 2. Object map syntax (rich errors with extensions) problem(config: map) -> map ``` ## Parameters ### Positional syntax | Parameter | Type | Required | Description | | :------------ | :------- | :------- | :----------------------------------------------------------------- | | `status` | `int` | yes | HTTP status code (e.g. `400`, `404`, `409`, `422`, `500`) | | `detail` | `string` | yes | Human-readable explanation specific to this occurrence | | `custom_type` | `string` | no | Custom problem type URI or slug (defaults to slugified HTTP title) | ### Object map syntax | Field | Type | Required | Description | | :--------- | :------- | :------- | :------------------------------------------------------------------ | | `status` | `int` | yes | HTTP status code | | `detail` | `string` | yes | Human-readable explanation | | `title` | `string` | no | Custom title (defaults to standard `http.StatusText`) | | `type` | `string` | no | Custom type URI or slug (defaults to URN or `problem.type_prefix`) | | `instance` | `string` | no | Request path identifier (defaults to current route path) | | `*` | `any` | no | Any additional key-value pairs are preserved as RFC 9457 extensions | ## Automatic field derivation To minimize boilerplate in manifests, `problem()` automatically derives missing fields: 1. **Title:** If `title` is omitted, the engine uses the canonical HTTP status text for `status` (e.g. `404` -> `"Not Found"`, `409` -> `"Conflict"`). 2. **Type URI:** If `type` is omitted, the title is slugified and appended to the configured URI scheme: - **Default:** `"urn:hclapi:error:"` (e.g. `"urn:hclapi:error:not-found"`). - **If `problem.type_prefix` is set in `server {}`:** `problem.type_prefix + ""` (e.g. `"https://docs.example.com/errors/not-found"`). 3. **Instance:** Defaults to the current request's URL path (`ctx.request.path`). *** ## Examples ### 1. Simple 404 not found response (Positional) ```hcl endpoint "GET /api/v1/users/{id}" { pipeline { sql "find_user" { connection = connection.postgres.main query = "SELECT id, name FROM users WHERE id = @id" args = { id = ctx.request.path.id } } respond { condition = steps.find_user.rows_affected == 0 status = 404 body = problem(404, "User with ID ${ctx.request.path.id} not found") } respond { status = 200 body = steps.find_user.row } } } ``` #### Serialized output: ```json { "type": "urn:hclapi:error:not-found", "title": "Not Found", "status": 404, "detail": "User with ID 42 not found", "instance": "/api/v1/users/42" } ``` *** ### 2. Database constraint collision with custom slug (Positional) ```hcl sql "insert_user" { connection = connection.postgres.main query = "INSERT INTO users (email) VALUES (@email)" args = { email = ctx.request.body.email } catch "23505" { status = 409 body = problem(409, "Email address is already registered", "email-collision") } } ``` #### Serialized output: ```json { "type": "urn:hclapi:error:email-collision", "title": "Conflict", "status": 409, "detail": "Email address is already registered", "instance": "/api/v1/users" } ``` *** ### 3. Validation failure with RFC 9457 extensions (Map syntax) ```hcl respond { condition = ctx.request.body.age < 18 status = 422 body = problem({ status = 422 title = "Unprocessable Entity" detail = "User must be at least 18 years old" error_code = "AGE_RESTRICTION" invalid_params = [ { name = "age", reason = "must be greater than or equal to 18" } ] }) } ``` #### Serialized output: ```json { "type": "urn:hclapi:error:unprocessable-entity", "title": "Unprocessable Entity", "status": 422, "detail": "User must be at least 18 years old", "instance": "/api/v1/users", "error_code": "AGE_RESTRICTION", "invalid_params": [ { "name": "age", "reason": "must be greater than or equal to 18" } ] } ``` ## Errors - Fails with an evaluation error if `status` is not an integer or if required arguments are missing. - Fails with an evaluation error if a single argument is passed that is not a `map` or `number`. --- url: /hclapi/docs/manifest/functions/encoding/json_encode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # json\_encode Serializes any HCL data structure (primitive, map, object, list) into a valid JSON string. ## Signature ```hcl json_encode(value: any) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :--------------------------------------- | | value | any | yes | The value or data structure to serialize | ## Return value Returns the documented result for the function signature. ## Example ```hcl endpoint "GET /api/v1/products/{id}" { pipeline { redis "cache_write" { connection = connection.redis.cache command = "SET" key = "product:${ctx.request.path.id}" value = json_encode(steps.find_product.result) ttl = "30m" } } } ``` --- url: /hclapi/docs/manifest/functions/encoding/json_decode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # json\_decode Parses a JSON string into corresponding HCL primitives, lists, or maps. ## Signature ```hcl json_decode(str: string) -> any ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------------------- | | str | string | yes | The JSON string to parse | ## Return value Returns the documented result for the function signature. ## Example ```hcl redis "cache_lookup" { connection = connection.redis.cache command = "GET" key = "product:${ctx.request.path.id}" } respond { condition = steps.cache_lookup.value != null status = 200 body = json_decode(steps.cache_lookup.value) } ``` --- url: /hclapi/docs/manifest/functions/encoding/base64_encode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # base64\_encode Encodes a string using standard RFC 4648 Base64 encoding. ## Signature ```hcl base64_encode(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :------------------------------ | | str | string | yes | The plain text string to encode | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { basic_token = base64_encode("${ctx.request.body.user}:${ctx.request.body.pass}") } } ``` --- url: /hclapi/docs/manifest/functions/encoding/base64_decode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # base64\_decode Decodes a standard Base64 encoded string back to plain text. ## Signature ```hcl base64_decode(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :-------------------------- | | str | string | yes | The Base64 string to decode | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { value = base64_decode(ctx.request.headers.x_token) } } ``` --- url: /hclapi/docs/manifest/functions/encoding/url_encode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # url\_encode Escapes characters in a string to make it safe for inclusion inside a URL query parameter. ## Signature ```hcl url_encode(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :------------------- | | str | string | yes | The string to escape | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 302 headers = { "Location" = "https://idp.example.com/oauth?redirect_uri=${url_encode(ctx.request.query.callback)}" } } ``` --- url: /hclapi/docs/manifest/functions/encoding/url_decode.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # url\_decode Unescapes a URL percent-encoded string. ## Signature ```hcl url_decode(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :------------------------------- | | str | string | yes | The URL encoded string to decode | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { callback = url_decode(ctx.request.query.callback) } } ``` --- url: /hclapi/docs/manifest/functions/strings/lower.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # lower Converts all characters in a string to lowercase. ## Signature ```hcl lower(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | str | string | yes | Input string | ## Return value Returns the documented result for the function signature. ## Example ```hcl key = "user:${lower(ctx.request.path.username)}" ``` --- url: /hclapi/docs/manifest/functions/strings/upper.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # upper Converts all characters in a string to uppercase. ## Signature ```hcl upper(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | str | string | yes | Input string | ## Return value Returns the documented result for the function signature. ## Example ```hcl body = { country = upper(ctx.request.query.country) } ``` --- url: /hclapi/docs/manifest/functions/strings/trim_space.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # trim\_space Removes leading and trailing whitespace characters (spaces, tabs, newlines) from a string. ## Signature ```hcl trim_space(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | str | string | yes | Input string | ## Return value Returns the documented result for the function signature. ## Example ```hcl args = { query = "%${trim_space(ctx.request.query.q)}%" } ``` --- url: /hclapi/docs/manifest/functions/strings/trim.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # trim Removes characters defined in `cutset` from both ends of a string. ## Signature ```hcl trim(str: string, cutset: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :---------- | :------------- | :------- | :---------------------- | | str, cutset | string, string | yes | Input string and cutset | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { normalized = trim(ctx.request.query.value, " -") } } ``` --- url: /hclapi/docs/manifest/functions/strings/trim_prefix.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # trim\_prefix Removes a prefix from the start of a string if present. ## Signature ```hcl trim_prefix(str: string, prefix: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :---------- | :------------- | :------- | :---------------------- | | str, prefix | string, string | yes | Input string and prefix | ## Return value Returns the documented result for the function signature. ## Example ```hcl args = { token = trim_prefix(ctx.request.headers.authorization, "Bearer ") } ``` --- url: /hclapi/docs/manifest/functions/strings/trim_suffix.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # trim\_suffix Removes a suffix from the end of a string if present. ## Signature ```hcl trim_suffix(str: string, suffix: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :---------- | :------------- | :------- | :---------------------- | | str, suffix | string, string | yes | Input string and suffix | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { file = trim_suffix(ctx.request.query.file, ".json") } } ``` --- url: /hclapi/docs/manifest/functions/strings/split.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # split Splits a string into a list of substrings separated by `separator`. ## Signature ```hcl split(separator: string, str: string) -> list(string) ``` ## Parameters | Parameter | Type | Required | Description | | :------------- | :------------- | :------- | :-------------------------- | | separator, str | string, string | yes | Separator and source string | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { filters = split(",", coalesce(ctx.request.query.filters, "")) } } ``` --- url: /hclapi/docs/manifest/functions/strings/join.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # join Concatenates elements in a list of strings using a `separator`. ## Signature ```hcl join(separator: string, list: list(string)) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------------- | :------------------- | :------- | :------------------- | | separator, list | string, list(string) | yes | Separator and values | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { key = join(":", ["GET", ctx.request.path.id, "COMPLETED"]) } } ``` --- url: /hclapi/docs/manifest/functions/strings/replace.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # replace Replaces all occurrences of a search string with a replacement string. ## Signature ```hcl replace(str: string, search: string, replace: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :------------------- | :--------------------- | :------- | :-------------------------- | | str, search, replace | string, string, string | yes | Source, search, replacement | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { slug = replace(lower(ctx.request.body.name), " ", "-") } } ``` --- url: /hclapi/docs/manifest/functions/strings/format.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # format Formats a string using standard `printf` style verbs. ## Signature ```hcl format(format: string, ...args: any) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :----------- | :---------- | :------- | :------------------------- | | format, args | string, any | yes | Format template and values | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { message = format("User %s has %d items in cart", ctx.request.path.name, 4) } } ``` --- url: /hclapi/docs/manifest/functions/collections/coalesce.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # coalesce Evaluates arguments sequentially and returns the first argument that is not `null` and not an empty string `""`. ## Signature ```hcl coalesce(...values: any) -> any ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :--------- | :--------------- | | ...values | any | at least 1 | Candidate values | ## Return value Returns the documented result for the function signature. ## Example ```hcl args = { limit = coalesce(ctx.request.query.limit, 20) offset = coalesce(ctx.request.query.offset, 0) } ``` --- url: /hclapi/docs/manifest/functions/collections/length.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # length Returns the total number of elements in a list, keys in a map, or characters in a string. ## Signature ```hcl length(collection: any) -> int ``` ## Parameters | Parameter | Type | Required | Description | | :--------- | :--- | :------- | :------------------------------ | | collection | any | yes | Collection or string to measure | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { total = length(steps.fetch_orders.result) } } ``` --- url: /hclapi/docs/manifest/functions/collections/merge.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # merge Merges two or more maps into a single map. If duplicate keys exist, values from later arguments take precedence. ## Signature ```hcl merge(...maps: map) -> map ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :--------- | :------------ | | maps | map | at least 2 | Maps to merge | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = merge(steps.find_user.result, { retrieved_at = now() }) } ``` --- url: /hclapi/docs/manifest/functions/collections/lookup.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # lookup Retrieves the value of a single key from a map, returning a fallback default if the key does not exist. ## Signature ```hcl lookup(map: map, key: string, default: any) -> any ``` ## Parameters | Parameter | Type | Required | Description | | :---------------- | :--------------- | :------- | :------------------------------ | | map, key, default | map, string, any | yes | Source map, key, fallback value | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200 body = { role = lookup(ctx.request.body, "role", "standard_user") } } ``` --- url: /hclapi/docs/manifest/functions/collections/keys.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # keys Returns a list containing all keys from a map, sorted lexically. ## Signature ```hcl keys(map: map) -> list(string) ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :---------- | | map | map | yes | Source map | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = keys(ctx.request.body) } ``` --- url: /hclapi/docs/manifest/functions/collections/values.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # values Returns a list containing all values from a map. ## Signature ```hcl values(map: map) -> list(any) ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :--- | :------- | :---------- | | map | map | yes | Source map | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = values(ctx.request.body) } ``` --- url: /hclapi/docs/manifest/functions/collections/contains.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # contains Determines whether a list contains a specific value. ## Signature ```hcl contains(list: list, value: any) -> bool ``` ## Parameters | Parameter | Type | Required | Description | | :---------- | :-------- | :------- | :----------------------- | | list, value | list, any | yes | List and candidate value | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { condition = !contains(["admin", "editor"], ctx.request.headers.x_role) status = 403 body = { error = "Forbidden: Insufficient privileges" } } ``` --- url: /hclapi/docs/manifest/functions/cryptography/sha256.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # sha256 Computes the SHA-256 cryptographic hash of a string, returning a lowercase 64-character hexadecimal digest. ## Signature ```hcl sha256(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | str | string | yes | Input string | ## Return value Returns the documented result for the function signature. ## Example ```hcl redis "cache_query" { connection = connection.redis.cache command = "SET" key = "cache:query:${sha256(json_encode(ctx.request.body))}" value = json_encode(steps.db_query.result) ttl = "15m" } ``` --- url: /hclapi/docs/manifest/functions/cryptography/md5.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # md5 Computes the MD5 checksum of a string, returning a 32-character hexadecimal digest. ## Signature ```hcl md5(str: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | str | string | yes | Input string | ## Return value Returns the documented result for the function signature. ## Example :::warning Security note MD5 is suitable for checksums such as cache keys or weak ETags, not for security-sensitive hashing. ::: ```hcl respond { status = 200 headers = { "ETag" = format("\"%s\"", md5(json_encode(steps.find_user.result))) } body = steps.find_user.result } ``` --- url: /hclapi/docs/manifest/functions/cryptography/hmac_sha256.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # hmac\_sha256 Computes an HMAC-SHA256 signature for a payload using a shared secret key. Returns a lowercase hexadecimal string. ## Signature ```hcl hmac_sha256(key: string, message: string) -> string ``` ## Parameters | Parameter | Type | Required | Description | | :----------- | :------------- | :------- | :--------------------- | | key, message | string, string | yes | Shared key and message | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { condition = ctx.request.headers.x_hub_signature != hmac_sha256(env("WEBHOOK_SECRET"), json_encode(ctx.request.body)) status = 401 body = { error = "Invalid webhook signature" } } ``` --- url: /hclapi/docs/manifest/functions/math/min.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # min Returns the smallest value from a sequence of numbers. ## Signature ```hcl min(...numbers: number) -> number ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :--------- | :--------------- | | numbers | number | at least 1 | Candidate values | ## Return value Returns the documented result for the function signature. ## Example ```hcl args = { limit = min(coalesce(ctx.request.query.limit, 20), 100) } ``` --- url: /hclapi/docs/manifest/functions/math/max.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # max Returns the largest value from a sequence of numbers. ## Signature ```hcl max(...numbers: number) -> number ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :--------- | :--------------- | | numbers | number | at least 1 | Candidate values | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { retries = max(0, ctx.request.query.retries) } } ``` --- url: /hclapi/docs/manifest/functions/math/abs.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # abs Returns the absolute value of a number. ## Signature ```hcl abs(num: number) -> number ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | num | number | yes | Input number | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { distance = abs(ctx.request.query.offset) } } ``` --- url: /hclapi/docs/manifest/functions/math/ceil.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # ceil Rounds a floating-point number up to the nearest integer. ## Signature ```hcl ceil(num: number) -> int ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | num | number | yes | Input number | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { pages = ceil(ctx.request.query.items / 25) } } ``` --- url: /hclapi/docs/manifest/functions/math/floor.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # floor Rounds a floating-point number down to the nearest integer. ## Signature ```hcl floor(num: number) -> int ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------- | | num | number | yes | Input number | ## Return value Returns the documented result for the function signature. ## Example ```hcl respond { status = 200, body = { bucket = floor(ctx.request.query.score) } } ``` --- url: /hclapi/docs/manifest/functions/math/parse_int.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # parse\_int Parses an integer from a string representation in the specified base (2 to 36). ## Signature ```hcl parse_int(str: string, base: int) -> int ``` ## Parameters | Parameter | Type | Required | Description | | :-------- | :---------- | :------- | :---------------------------- | | str, base | string, int | yes | Numeric string and radix 2–36 | ## Return value Returns the documented result for the function signature. ## Example ```hcl sql "find_by_bitmask" { connection = connection.postgres.main query = "SELECT id FROM records WHERE flags = @mask" args = { mask = parse_int(ctx.request.query.hex_flag, 16) } } ``` --- url: /hclapi/docs/steps/sql.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # sql Executes a parameterized SQL query or mutation against a database connection pool. Parameters are bound through prepared statements using `@name` placeholders. ## Declaration ```hcl sql "find_user" { connection = connection.postgres.main query = <<-SQL SELECT id, name, email FROM users WHERE id = @id SQL args = { id = ctx.request.path.id } } ``` ## Attributes | Attribute | Type | Required | Description | | :--------------- | :----------- | :------- | :----------------------------------------------------- | | `label` | `string` | yes | Step identifier; outputs are written to `steps.` | | `connection` | `connection` | yes | Database connection pool reference | | `query` | `string` | yes | Query or mutation; parameters use `@name` | | `args` | `map` | no | Context values bound to query parameters | | `catch ""` | `block` | no | Handles specific database error codes | ## Exported outputs | Field | Type | Description | | :--------------------------- | :-------------- | :------------------------------------------- | | `steps..rows` | `list(map)` | All returned rows; `[]` if no rows match | | `steps..row` | `map` or `null` | First returned row | | `steps..rows_affected` | `int` | Rows returned, inserted, updated, or deleted | ## Fetch a single record ```hcl endpoint "GET /api/v1/users/{id}" { pipeline { sql "find_user" { connection = connection.postgres.main query = "SELECT id, name, email FROM users WHERE id = @id" args = { id = ctx.request.path.id } } respond { condition = steps.find_user.rows_affected == 0 status = 404 body = { error = "User not found" } } respond { status = 200 body = steps.find_user.row } } } ``` ## Fetch a list ```hcl endpoint "GET /api/v1/users" { pipeline { sql "list_users" { connection = connection.postgres.main query = "SELECT id, name, email FROM users ORDER BY id LIMIT 50" } respond { status = 200 body = steps.list_users.rows } } } ``` ## Catch database constraint errors ```hcl sql "insert_user" { connection = connection.postgres.main query = <<-SQL INSERT INTO users (email, full_name) VALUES (@email, @full_name) RETURNING id, email, full_name, created_at SQL args = { email = ctx.request.body.email full_name = ctx.request.body.full_name } catch "23505" { status = 409 body = { error = "A user with this email address already exists." } } } ``` ## Common database error codes | Error | PostgreSQL / CockroachDB | SQLite | MySQL / MariaDB | SQL Server | | :----------------------- | :----------------------: | :-----------: | :-------------: | :-------------: | | Unique violation | `23505` | `2067` / `19` | `1062` | `2627` / `2601` | | Foreign key violation | `23503` | `787` / `19` | `1452` | `547` | | Not null violation | `23502` | `1299` / `19` | `1048` | `515` | | Check constraint failure | `23514` | `275` / `19` | `3819` | `547` | | Deadlock / serialization | `40001` / `40P01` | `5` | `1213` | `1205` | See the official [PostgreSQL](https://www.postgresql.org/docs/current/errcodes-appendix.html), [SQLite](https://www.sqlite.org/rescode.html), [MySQL](https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html), and [SQL Server](https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors) error references. --- url: /hclapi/docs/steps/starlark.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # starlark Executes sandboxed Starlark for data transformation, filtering, and business logic. ## Declaration ```hcl starlark "" { source = <<-STARLARK def execute(ctx): return { "status": "processed" } STARLARK } ``` The script must define `execute(ctx)`, returning a dict, list, or scalar. `ctx` mirrors the [execution context](/hclapi/docs/concepts/context.md), using dictionary access instead of dotted attributes. ## Attributes | Attribute | Type | Required | Description | | :-------- | :------- | :------- | :-------------------------------------- | | `label` | `string` | yes | Step identifier | | `source` | `string` | yes | Starlark source defining `execute(ctx)` | ## Example: defaults and lookups ```python def execute(ctx): prefix = ctx.request.body.get("prefix", "default_prefix") tags = ctx.request.body.get("tags", []) user_id = ctx.request.body["user_id"] return {"prefix": prefix, "total_tags": len(tags), "user_id": user_id} ``` ## Example: list comprehension ```python def execute(ctx): raw_tags = ctx.request.body.get("tags", []) prefix = ctx.request.body.get("prefix", "tag") cleaned = [prefix + ":" + t.strip().lower() for t in raw_tags if len(t.strip()) > 0] return {"count": len(cleaned), "tags": cleaned} ``` ## Example: reshape prior step results ```python def execute(ctx): account = ctx.steps.fetch_account invoices = ctx.steps.fetch_invoices total_spent = sum([inv["amount_cents"] for inv in invoices if inv["status"] == "paid"]) return { "account_id": account["id"], "name": account["name"].strip().title(), "total_spent_cents": total_spent } ``` :::note Sandboxing Starlark scripts have no filesystem or network access. Unbounded recursion and infinite loops are prevented by the runtime. ::: --- url: /hclapi/docs/steps/redis.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # redis Executes key-value commands against a Redis or Valkey connection pool. Supports cache reads, writes with TTL, key deletions, and counters. ## Declaration ```hcl redis "cache_lookup" { connection = connection.redis.cache command = "GET" key = "product:${ctx.request.path.sku}" } ``` ## Attributes | Attribute | Type | Required | Description | | :----------- | :----------- | :-------- | :----------------------------------------- | | `label` | `string` | yes | Step identifier | | `connection` | `connection` | yes | Redis connection pool reference | | `command` | `string` | yes | `GET`, `SET`, `DEL`, `INCR`, or `EXISTS` | | `key` | `string` | yes | Key; supports dynamic string interpolation | | `value` | `any` | for `SET` | Value to store | | `ttl` | `Duration` | no | Expiration duration for `SET` | ## Output `steps..value` contains the retrieved cache value, `"OK"` for `SET`, a count for `DEL`, or an integer for `INCR`. ## Cache-aside pattern ```hcl endpoint "GET /api/v1/products/{sku}" { pipeline { redis "cache_lookup" { connection = connection.redis.cache command = "GET" key = "cache:product:${ctx.request.path.sku}" } respond { condition = steps.cache_lookup.value != null status = 200 headers = { "X-Cache" = "HIT" } body = json_decode(steps.cache_lookup.value) } sql "db_query" { connection = connection.postgres.main query = "SELECT id, sku, name, price FROM products WHERE sku = @sku" args = { sku = ctx.request.path.sku } } respond { condition = steps.db_query.rows_affected == 0 status = 404 body = { error = "Product not found" } } redis "cache_write" { connection = connection.redis.cache command = "SET" key = "cache:product:${ctx.request.path.sku}" value = json_encode(steps.db_query.row) ttl = "30m" } respond { status = 200 headers = { "X-Cache" = "MISS" } body = steps.db_query.row } } } ``` --- url: /hclapi/docs/steps/transaction.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # transaction Groups multiple `sql` steps into one atomic transaction. A failure in any nested query rolls back the entire block. ## Declaration ```hcl transaction "provision" { connection = connection.postgres.main sql "step_one" { # ... } sql "step_two" { # ... } } ``` ## Attributes | Attribute | Type | Required | Description | | :----------- | :----------- | :----------- | :------------------------------------------- | | `label` | `string` | yes | Transaction identifier | | `connection` | `connection` | yes | Pool used to acquire the transaction handle | | `sql` blocks | `block` | at least one | Executed sequentially inside the transaction | Nested steps may reference each other's results in the normal way. ## Rollback semantics ### 1. Query failure A query error triggers an immediate `ROLLBACK`. ### 2. Caught constraint failure A `catch` block that aborts with a status also rolls back and writes its error payload directly to the client. ### 3. Successful completion If every statement succeeds, the engine issues `COMMIT` before continuing with the next pipeline step. See [Transactional writes](/hclapi/docs/patterns.md#transactional-writes) for a complete example. --- url: /hclapi/docs/steps/parallel.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # parallel Executes multiple steps concurrently, each on a separate goroutine, and waits for all branches to complete. ## Declaration ```hcl parallel { sql "fetch_account" { # branch 1 } sql "fetch_invoices" { # branch 2 } redis "fetch_metrics" { # branch 3 } } ``` ## Semantics 1. Each branch executes independently and cannot see other branch results until the block completes. 2. Execution pauses at the closing brace until every branch finishes. 3. An error in any branch cancels the remaining branches and fails the pipeline. 4. Branch outputs are available under `ctx.steps.` after the block completes. See [Parallel aggregation](/hclapi/docs/patterns.md#parallel-aggregation) for a complete example. --- url: /hclapi/docs/steps/go.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # go Invokes a native Go function registered on the engine. Use this step for logic that cannot be expressed in Starlark or SQL: proprietary business rules, third-party SDKs, outbound HTTP clients, or hardware cryptography. ## Declaration ```hcl go "" { use = "" args = { city = ctx.request.path.city } } ``` ## Attributes | Attribute | Type | Required | Description | | :-------- | :------- | :------- | :------------------------------------------ | | `label` | `string` | yes | Step identifier; written to `steps.` | | `use` | `string` | yes | Function name registered on the `Engine` | | `args` | `map` | no | Evaluated arguments passed into `step.Args` | ## Registration in Go ```go engine.RegisterStep("services.weather_lookup", func(ctx context.Context, step *hclapi.Step) (any, error) { city := step.Args.GetOr("city", "") if city == "" { return nil, errors.New("missing or empty 'city' argument") } // Access incoming request headers safely authHeader := step.Request.Header("Authorization") return map[string]any{ "city": strings.ToLower(strings.TrimSpace(city)), "temperature_c": 22.5, "condition": "Sunny", }, nil }) ``` The return value is exported under `steps..result`. :::warning Panic handling Panics in a registered function are automatically recovered by the engine, converted to an RFC 9457 500 error response, and don't terminate the server process. ::: --- url: /hclapi/docs/steps/respond.md --- > For AI agents: the complete documentation index is available at /hclapi/llms.txt, the full documentation bundle is available at /hclapi/llms-full.txt. # respond Terminates the pipeline. Sets the status, headers, and body. No step after `respond` runs once it fires. ## Declaration ```hcl respond { condition = steps.find_user.rows_affected == 0 status = 404 headers = { "Cache-Control" = "no-store" "X-Trace-ID" = uuid() } body = { error = "User not found" } } ``` ## Attributes | Attribute | Type | Default | Description | | :---------- | :-------------------- | :------ | :--------------------------------- | | `condition` | `Expression` | `true` | Step is skipped if `false` | | `status` | `int` or `Expression` | `200` | HTTP status code | | `headers` | `map` or `Expression` | `{}` | Dynamic or static response headers | | `body` | `any` or `Expression` | `null` | Payload to serialize | ## Serialization - If `Content-Type` is absent, hclapi sets `application/json` and JSON-encodes `body`. - If a custom content type is provided and `body` is a string or byte slice, the raw payload is written. - Header names and values are sanitized to remove carriage-return and newline characters, preventing response-splitting attacks.