Templates
Author, import, and manage HCL template files that define constrained API operations
Templates are HCL files that define constrained operations for AI agents and CLI users. Each template declares a provider, one or more commands, typed parameters, a protocol-specific operation, and a result mapping. Earl loads templates from two directories:
- Local (workspace):
./templates/relative to the current working directory - Global:
~/.config/earl/templates/
Local templates override global ones when both define the same provider.command key.
Authoring a Template
Every template file follows this structure:
version = 1
provider = "github"
categories = ["scm"]
command "search_issues" {
title = "Search Issues"
summary = "Search GitHub issues using a query string"
description = <<-EOF
Search for GitHub issues and pull requests using GitHub's search syntax.
## Guidance for AI agents
Use this command to find issues matching a natural-language query.earl call github.search_issues --query "is:open label:bug"
EOF
annotations {
mode = "read"
secrets = ["github.token"]
}
param "query" {
type = "string"
required = true
description = "GitHub search query"
}
param "per_page" {
type = "integer"
required = false
default = 10
description = "Results per page"
}
operation {
protocol = "http"
method = "GET"
url = "https://api.github.com/search/issues"
headers = {
Accept = "application/vnd.github+json"
X-GitHub-Api-Version = "2022-11-28"
}
query = {
q = "{{ args.query }}"
per_page = "{{ args.per_page }}"
}
auth {
kind = "bearer"
secret = "github.token"
}
}
result {
decode = "json"
output = "{{ result }}"
}
}For a complete field-by-field reference, see the Template Schema Reference.
Root Fields
| Field | Type | Required | Description |
|---|---|---|---|
version | integer | Yes | Must be 1. |
provider | string | Yes | Namespace for commands (e.g., github). |
categories | string[] | No | Provider-level categories applied to all commands. |
command | block(s) | Yes | One or more named command blocks. |
Command Blocks
Use the HCL block syntax command "<name>" { ... } to define commands. Each command must include title, summary, description, annotations, operation, and result. Parameters are optional.
Parameters
Parameters define the typed inputs that users pass via --name value on the CLI or as MCP tool arguments.
param "query" {
type = "string"
required = true
description = "Search query"
}
param "limit" {
type = "integer"
default = 10
}Supported types: string, integer, number, boolean, null, array, object.
Annotations
Every command declares a mode and lists the secret keys it needs:
annotations {
mode = "read"
secrets = ["github.token"]
}mode--"read"or"write". Write-mode commands require user confirmation before execution (unless--yesis passed).secrets-- Array of secret key names the command is allowed to access. Auth blocks that reference a secret must list it here.
Template Expressions
Template strings use Jinja2 syntax ({{ ... }}) rendered by minijinja with strict undefined behavior. Two context objects are available during operation rendering:
| Variable | Available In | Description |
|---|---|---|
args.* | operation, result | Bound parameter values. |
secrets.* | operation only | Plaintext secret values (dotted keys are nested). |
result | result.output only | Decoded (and optionally extracted) response. |
secrets.* is only available in operation rendering -- never in
result.output. This prevents secrets from leaking through command output.
HCL is parsed before template expressions are rendered. This means {{ }} expressions must appear inside valid HCL tokens. For example, in array literals use string-wrapped expressions: params = ["{{ args.limit }}"] instead of params = [{{ args.limit }}]. Pure expressions like "{{ args.limit }}" are automatically parsed as JSON after rendering, so numeric and boolean types are preserved.
HCL Functions
Template HCL files support built-in functions for loading external content:
| Function | Description |
|---|---|
file("path") | Read a file relative to the template directory. Path must be relative and must not contain ... |
base64encode("value") | Base64-encode a string. |
trimspace("value") | Trim leading and trailing whitespace. |
Functions can be composed: trimspace(file("query.sql")).
Protocol Examples
Earl supports five operation protocols. Each protocol has its own operation shape.
Standard HTTP requests with full control over method, URL, path, query, headers, cookies, auth, body, and transport.
version = 1
provider = "github"
categories = ["scm"]
command "search_issues" {
title = "Search Issues"
summary = "Search GitHub issues using a query string"
description = "Search for GitHub issues and pull requests."
annotations {
mode = "read"
secrets = ["github.token"]
}
param "query" {
type = "string"
required = true
}
param "per_page" {
type = "integer"
default = 10
}
operation {
protocol = "http"
method = "GET"
url = "https://api.github.com/search/issues"
headers = {
Accept = "application/vnd.github+json"
}
query = {
q = "{{ args.query }}"
per_page = "{{ args.per_page }}"
}
auth {
kind = "bearer"
secret = "github.token"
}
}
result {
decode = "json"
extract {
json_pointer = "/items"
}
output = "Found {{ result | length }} issues"
}
}GraphQL operations send a query, optional variables, and optional operation name over HTTP POST.
version = 1
provider = "myapi"
categories = ["api"]
command "get_user" {
title = "Get User"
summary = "Fetch a user by ID via GraphQL"
description = "Queries the GraphQL API for a user by their ID."
annotations {
mode = "read"
secrets = ["myapi.token"]
}
param "user_id" {
type = "string"
required = true
}
operation {
protocol = "graphql"
url = "https://api.example.com/graphql"
auth {
kind = "bearer"
secret = "myapi.token"
}
graphql {
query = "query User($id: ID!) { user(id: $id) { login email } }"
operation_name = "User"
variables = {
id = "{{ args.user_id }}"
}
}
}
result {
decode = "json"
output = "{{ result }}"
}
}gRPC operations call a service method over HTTP/2. Earl uses server reflection by default; provide descriptor_set_file when reflection is unavailable.
version = 1
provider = "health"
categories = ["infrastructure"]
command "check" {
title = "Health Check"
summary = "Check gRPC service health"
description = "Calls the gRPC health check endpoint."
annotations {
mode = "read"
secrets = ["service.token"]
}
operation {
protocol = "grpc"
url = "http://127.0.0.1:50051"
headers = {
x-trace-id = "{{ args.trace_id }}"
}
auth {
kind = "bearer"
secret = "service.token"
}
grpc {
service = "grpc.health.v1.Health"
method = "Check"
body = { service = "" }
}
}
param "trace_id" {
type = "string"
default = ""
}
result {
decode = "json"
output = "{{ result }}"
}
}For gRPC, auth.api_key.location must be header. The
transport.proxy_profile and transport.tls.min_version options are not
supported for gRPC.
Bash operations execute shell scripts in a sandboxed environment. The bash feature flag must be enabled.
version = 1
provider = "system"
categories = ["system", "bash"]
command "disk_usage" {
title = "Check disk usage"
summary = "Reports disk usage for a given path"
description = "Runs du -sh in a sandboxed bash environment."
annotations {
mode = "read"
secrets = []
}
param "path" {
type = "string"
required = true
description = "Filesystem path to check"
}
operation {
protocol = "bash"
bash {
script = "du -sh {{ args.path }}"
sandbox {
network = false
}
}
}
result {
decode = "text"
output = "{{ result }}"
}
}SQL operations execute parameterized queries against a database. The connection URL is stored as a secret. Jinja2 template expressions are not allowed in sql.query -- use ? placeholders and sql.params instead.
version = 1
provider = "analytics"
categories = ["analytics", "sql"]
command "recent_orders" {
title = "Recent orders"
summary = "Fetches recent orders from the database"
description = "Runs a read-only SQL query to fetch recent orders."
annotations {
mode = "read"
secrets = ["analytics.database_url"]
}
param "limit" {
type = "integer"
default = 10
}
operation {
protocol = "sql"
sql {
connection_secret = "analytics.database_url"
query = "SELECT id, customer, total FROM orders ORDER BY created_at DESC LIMIT ?"
params = ["{{ args.limit }}"]
sandbox {
read_only = true
max_rows = 100
}
}
}
result {
decode = "json"
output = "Found {{ result | length }} orders"
}
}The sql.connection_secret must be declared in annotations.secrets. Earl
validates this at load time.
Importing Templates
Import templates from a local file path or a direct HTTP(S) URL.
Import from a local file
earl templates import ./my-template.hcl
earl templates import /opt/templates/github.hclImport from a remote URL
earl templates import https://example.com/templates/github.hclChoose the import scope
By default, templates import to the local ./templates/ directory. Use --scope global to import to ~/.config/earl/templates/:
earl templates import ./github.hcl --scope globalSet up required secrets
After importing, earl reports any secrets declared by the template:
Imported template from `./github.hcl` to `./templates/github.hcl`.
Required secrets:
- github.token
Set up with:
earl secrets set github.tokenRequirements:
- The source must be a
.hclfile. - Remote sources must use
http://orhttps://. - Import fails if the destination file already exists.
- Remote templates have a maximum size of 1 MiB.
Managing Templates
List commands
earl templates list # List all commands
earl templates list --mode read # Filter by mode
earl templates list --category scm # Filter by category
earl templates list --json # JSON outputSemantic search
earl templates search "find open PRs" # Search with natural language
earl templates search "database query" --limit 5Validate templates
earl templates validate # Validate all template filesValidation checks:
- HCL syntax is correct
- Schema fields match expected types
- Required fields are present (title, summary, description, output)
- Secret references in auth blocks are declared in
annotations.secrets - Parameter names are unique within a command
- Protocol-specific constraints are met
Generating templates
Delegate template authoring to a coding CLI:
earl templates generate -- claude --dangerously-skip-permissionsEarl prompts for a description of the template you want, then sends a structured prompt to the coding CLI with schema constraints. The coding CLI generates a valid .hcl file and runs earl templates validate to verify it.
Calling Commands
Execute a template command using earl call:
earl call github.search_issues --query "is:open label:bug" --per_page 5The command key is provider.command. Parameters are passed as --name value pairs. Boolean parameters can be passed as bare flags (--verbose) or with explicit values (--verbose true).
Add --json for structured JSON output, or --yes to skip write-mode confirmation:
earl call github.create_issue --title "Bug report" --yes
earl call github.search_issues --query "test" --json