earl

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

FieldTypeRequiredDescription
versionintegerYesMust be 1.
providerstringYesNamespace for commands (e.g., github).
categoriesstring[]NoProvider-level categories applied to all commands.
commandblock(s)YesOne 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 --yes is 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:

VariableAvailable InDescription
args.*operation, resultBound parameter values.
secrets.*operation onlyPlaintext secret values (dotted keys are nested).
resultresult.output onlyDecoded (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:

FunctionDescription
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.hcl

Import from a remote URL

earl templates import https://example.com/templates/github.hcl

Choose 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 global

Set 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.token

Requirements:

  • The source must be a .hcl file.
  • Remote sources must use http:// or https://.
  • 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 output
earl templates search "find open PRs"      # Search with natural language
earl templates search "database query" --limit 5

Validate templates

earl templates validate                    # Validate all template files

Validation 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-permissions

Earl 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 5

The 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

On this page