Complete field reference for Earl template files — every block, field, and type.
This page is a field-by-field reference. For worked examples, see the protocol pages: HTTP , GraphQL , gRPC , Bash , SQL , Browser .
A template file is an HCL file with a fixed top-level shape:
version = 1
provider = "github"
categories = [ "scm" , "issues" ]
environments { ... } # optional
command "name" { ... } # one or more
Field Type Required Description versioninteger yes Schema version. Must be 1. providerstring yes Provider identifier. Used in earl call <provider>.<command>. categorieslist of strings no Labels applied to every command in the file. Used for discovery and filtering. environmentsblock no Provider-level environment definitions. See Environments . command "<name>"block yes Command definition. Repeatable. The name becomes the second part of earl call <provider>.<name>.
command "search_repos" {
title = "Search repositories"
summary = "Search GitHub repos by query"
description = "..."
categories = [ "search" ]
annotations { ... }
param "query" { ... }
operation { ... }
result { ... }
environment_overrides { ... }
}
Field Type Required Description titlestring yes Short display name shown in earl list. summarystring yes One-line description exposed to the agent in the MCP tool listing. descriptionstring yes Full description the agent reads to decide whether to call the command. Supports Markdown. categorieslist of strings no Labels for this specific command. Merged with provider-level categories for filtering. annotationsblock no Mode, secrets list, and environment switching flags. param "<name>"block no Parameter declaration. Repeatable. operationblock yes The request to execute. Shape varies by protocol. resultblock no How to decode and format the response. Defaults to decode = "auto" and output = "{{ result }}". environment_overridesblock no Per-environment operation replacements.
annotations {
mode = "read"
secrets = [ "github.token" ]
allow_environment_protocol_switching = false
}
Field Type Default Description modestring "write""read" or "write". Write mode prompts for confirmation unless the caller passes --yes. Use "read" for commands that only retrieve data.secretslist of strings []Keys the command needs. Earl checks that these exist before executing. Every key referenced in an auth block must appear here. allow_environment_protocol_switchingboolean falseWhen true, the active environment can switch the operation protocol (e.g. from http to grpc). Disabled by default as a safety measure.
Note: the mode field defaults to "write", not "read". Omitting the annotations block means the command requires confirmation. Always set mode = "read" explicitly for read-only commands.
param "per_page" {
type = "integer"
required = false
default = 30
description = "Results per page (max 100)"
}
Field Type Required Description typestring yes Parameter type. See types table below. requiredboolean no Whether the caller must supply this parameter. Defaults to false. defaultany no Default value used when the parameter is not supplied. Only valid when required = false. descriptionstring no Shown to the agent so it knows what to pass.
Type Description "string"UTF-8 text. "integer"Whole number. "number"Floating-point number. "boolean"true or false."array"JSON array. "object"JSON object. "null"Null value. Rarely needed.
Inside the operation block, parameters are available as args.<name>:
url = "https://api.github.com/repos/{{ args.owner }}/{{ args.repo }}"
The operation block shape depends on the protocol field. HTTP fields are flat at the operation level. Every other protocol wraps its fields in a nested block.
operation {
protocol = "http"
method = "GET" # GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
url = "https://api.example.com/items/{{ args.id }}"
path = "/v2/items" # optional; appended to url
stream = false
headers = { Accept = "application/json" }
query = { filter = "{{ args.filter }}" }
cookies = { session = "{{ args.session }}" }
auth { ... }
body { ... }
transport { ... }
}
Field Type Required Description protocolstring yes Must be "http". methodstring yes HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. urlstring yes Full URL. Supports Jinja expressions. pathstring no Path segment appended to url. Useful when the base URL comes from an environment variable. querymap no Query string parameters. Values support Jinja expressions. headersmap no Request headers. Values support Jinja expressions. cookiesmap no Cookies sent with the request. Values support Jinja expressions. authblock no Authentication. See auth block . bodyblock no Request body. See body block . streamboolean no Set true to enable streaming. Defaults to false. See Streaming . transportblock no Timeout, retries, redirects, TLS, proxy. See transport block .
operation {
protocol = "graphql"
url = "https://api.github.com/graphql"
auth {
kind = "bearer"
secret = "github.token"
}
graphql {
query = <<-GQL
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
stargazerCount
}
}
GQL
operation_name = "MyQuery" # optional
variables = {
owner = "{{ args.owner }}"
repo = "{{ args.repo }}"
}
}
transport { ... }
}
GraphQL operation-level fields:
Field Type Required Description protocolstring yes Must be "graphql". urlstring yes GraphQL endpoint URL. methodstring no Defaults to POST. Override only if the server requires GET. querymap no URL query parameters (distinct from the GraphQL query). headersmap no Request headers. Accept and Content-Type default to application/json. cookiesmap no Cookies sent with the request. authblock no Authentication. graphqlblock yes The GraphQL payload. streamboolean no Enable streaming. Defaults to false. transportblock no Transport settings.
graphql inner block fields:
Field Type Required Description querystring yes The GraphQL query or mutation. Heredoc syntax recommended for multi-line queries. operation_namestring no operationName sent in the request body. Required when the document contains multiple operations.variablesmap no Variables map. Values support Jinja expressions.
operation {
protocol = "grpc"
url = "https://grpc.example.com"
auth {
kind = "bearer"
secret = "provider.token"
}
grpc {
service = "example.v1.ExampleService"
method = "GetItem"
descriptor_set_file = "example.pb" # optional
body = {
id = "{{ args.id }}"
}
}
transport { ... }
}
gRPC operation-level fields:
Field Type Required Description protocolstring yes Must be "grpc". urlstring yes gRPC server URL. headersmap no gRPC metadata headers. authblock no Authentication. grpcblock yes gRPC call configuration. streamboolean no Enable server-streaming. Defaults to false. transportblock no Transport settings.
grpc inner block fields:
Field Type Required Description servicestring yes Fully qualified service name, e.g. example.v1.ExampleService. methodstring yes RPC method name, e.g. GetItem. bodymap no Request message fields. Values support Jinja expressions. descriptor_set_filestring no Path to a compiled .pb descriptor set file. Omit to use server reflection (gRPC reflection v1).
Earl uses gRPC reflection v1. If the server only exposes v1alpha or no reflection at all, provide a compiled descriptor set.
operation {
protocol = "bash"
bash {
script = <<-SH
jq -r '.[] | .name' {{ args.input_file }}
SH
env = {
MY_VAR = "{{ args.value }}"
}
cwd = "/tmp"
sandbox {
network = false
max_time_ms = 30000
max_output_bytes = 1048576
}
}
stream = false
}
Bash operation-level fields:
Field Type Required Description protocolstring yes Must be "bash". bashblock yes Script configuration. streamboolean no Stream stdout as lines. Defaults to false.
bash inner block fields:
Field Type Required Description scriptstring yes Shell script to execute. Supports Jinja expressions. envmap no Environment variables set before the script runs. Values support Jinja expressions. cwdstring no Working directory for the script. sandboxblock no Resource and network limits.
bash.sandbox fields:
Field Type Default Description networkboolean falseWhether the script can make network requests. writable_pathslist of strings []Filesystem paths the script may write to. max_time_msinteger — Wall-clock timeout in milliseconds. max_output_bytesinteger — Maximum combined stdout+stderr size in bytes. max_memory_bytesinteger — Memory limit in bytes. max_cpu_time_msinteger — CPU time limit in milliseconds.
Note: Earl also blocks all private and loopback IP ranges at the SSRF layer regardless of sandbox settings. network = true only enables outbound requests to public addresses.
operation {
protocol = "sql"
sql {
connection_secret = "myapp.db_url"
query = "SELECT id, name FROM users WHERE status = $1 LIMIT $2"
params = [ "{{ args.status }}" , "{{ args.limit }}" ]
sandbox {
read_only = true
max_rows = 100
max_time_ms = 5000
}
}
}
SQL operation-level fields:
Field Type Required Description protocolstring yes Must be "sql". sqlblock yes Query configuration. transportblock no Transport settings (connection-level).
sql inner block fields:
Field Type Required Description connection_secretstring yes Key name in the OS keychain whose value is the database connection URL. querystring yes SQL query with positional placeholders. Syntax varies by database: $1, $2... for PostgreSQL; ? for MySQL and SQLite. paramslist no Values for the positional placeholders. Each element supports Jinja expressions and must be quoted as an HCL string: ["{{ args.limit }}"]. Earl coerces the rendered value to the correct SQL type. sandboxblock no Query limits.
sql.sandbox fields:
Field Type Description read_onlyboolean Restrict the connection to read-only transactions. max_rowsinteger Limit the number of rows returned. max_time_msinteger Query execution timeout in milliseconds.
operation {
protocol = "browser"
browser {
session_id = "{{ args.session_id }}"
headless = true
timeout_ms = 30000
on_failure_screenshot = true
steps = [
{ action = "navigate" , url = "{{ args.url }}" },
{ action = "snapshot" },
]
}
}
Browser operation-level fields:
Field Type Required Description protocolstring yes Must be "browser". browserblock yes Browser configuration.
browser inner block fields:
Field Type Default Description stepslist of objects yes Ordered list of step objects to execute. Each object must include an action field. session_idstring — Stable identifier for a persistent browser session. Omit for a one-shot command. headlessboolean trueRun Chrome in headless mode. timeout_msinteger 30000Global timeout in milliseconds for the entire command. on_failure_screenshotboolean trueCapture a screenshot and attach it to the error output when any non-optional step fails.
Every step object has two cross-cutting optional fields in addition to its action-specific fields:
Field Default Description action— Required. Identifies the step type. See step reference below. optionalfalseWhen true, failure on this step is ignored and execution continues. timeout_ms— Per-step timeout in milliseconds, overrides the command-level timeout.
Action Key fields Result shape navigateurl (required), expected_status, timeout_ms{"ok": true}navigate_back— {"ok": true}navigate_forward— {"ok": true}reload— {"ok": true}
Action Key fields Result shape snapshot— {"text": "<accessibility tree>", "raw": [...]}screenshotpath, type (png/jpeg), full_page, ref{"data": "<base64>", "path": "..."}pdf_savepath{"path": "..."}
Action Key fields Result shape clickref or selector, double_click{"ok": true}hoverref or selector{"ok": true}fillref or selector, text, submit{"ok": true}fill_formfields (array of {ref/selector, value, type}){"ok": true}select_optionref or selector, values (array){"ok": true}checkref or selector{"ok": true}uncheckref or selector{"ok": true}press_keykey (e.g. "Enter", "Tab", "Escape"){"ok": true}dragstart_ref/start_selector, end_ref/end_selector{"ok": true}file_uploadref or selector, paths (array){"ok": true}handle_dialogaccept (boolean), prompt_text{"ok": true, "accept": ...}
Action Key fields Result shape mouse_movex, y{"ok": true}mouse_clickx, y, button{"ok": true}mouse_dragstart_x, start_y, end_x, end_y{"ok": true}mouse_downx, y, button{"ok": true}mouse_upx, y, button{"ok": true}mouse_wheeldelta_x, delta_y{"ok": true}
Action Key fields Result shape wait_fortext, text_gone, time (seconds), timeout_ms (optional — defaults to command timeout_ms){"ok": true}verify_text_visibletext{"ok": true}verify_element_visiblerole, accessible_name{"ok": true}verify_list_visibleitems (array){"ok": true}verify_valuevalue{"ok": true}
Action Key fields Result shape evaluatefunction (JS arrow function string), ref{"value": ...}run_codecode (JS statements string){"ok": true}
Action Key fields Result shape cookie_listdomain (filter, optional){"cookies": [...]}cookie_getname{"value": "..."}cookie_setname, value, domain, path, expires, http_only, secure{"ok": true}cookie_deletename{"ok": true}cookie_clear— {"ok": true}
Action Key fields Result shape local_storage_getkey{"value": "..."}local_storage_setkey, value{"ok": true}local_storage_deletekey{"ok": true}local_storage_clear— {"ok": true}session_storage_getkey{"value": "..."}session_storage_setkey, value{"ok": true}session_storage_deletekey{"ok": true}session_storage_clear— {"ok": true}storage_statepath (optional){"cookies": [...], "local_storage": {...}}set_storage_statepath{"ok": true}
Action Key fields Result shape tabsoperation (list/new/close/select), index{"tabs": [...]} or {"ok": true}resizewidth, height{"ok": true}close— {"ok": true}
Action Key fields Result shape routepattern, status, body, content_type{"ok": true}route_list— {"routes": [...]}unroutepattern{"ok": true}console_messages— {"messages": [...]}console_clear— {"ok": true}network_requests— {"requests": [...]}network_clear— {"ok": true}downloadpath{"path": "..."}
Action Key fields Result shape start_videowidth, height{"ok": true}stop_video— {"path": "..."}start_tracing— {"ok": true}stop_tracing— {"path": "..."}
Action Key fields Result shape generate_locatorref{"selector": "..."}
The body block inside an HTTP operation has a kind discriminator field.
body {
kind = "json"
value = {
name = "{{ args.name }}"
count = "{{ args.count }}"
}
}
Sends Content-Type: application/json. The value map supports nested objects and Jinja expressions.
body {
kind = "form_urlencoded"
fields = {
username = "{{ args.username }}"
password = "{{ args.password }}"
}
}
Sends Content-Type: application/x-www-form-urlencoded.
body {
kind = "multipart"
parts = [
{
name = "file"
file_path = "{{ args.path }}"
content_type = "application/octet-stream"
filename = "upload.bin"
},
{
name = "description"
value = "{{ args.description }}"
}
]
}
Sends Content-Type: multipart/form-data. Each part in the parts list has:
Field Required Description nameyes Form field name. valueone of three Text content for the part. Supports Jinja expressions. bytes_base64one of three Base64-encoded binary content. file_pathone of three Path to a file whose contents become the part body. Supports Jinja expressions. content_typeno MIME type for the part. filenameno Filename reported in the Content-Disposition header.
Exactly one of value, bytes_base64, or file_path is required per part.
body {
kind = "raw_text"
value = "{{ args.payload }}"
content_type = "text/xml"
}
Sends the rendered string as raw bytes with the given content_type (defaults to text/plain).
body {
kind = "raw_bytes_base64"
value = "{{ args.base64_data }}"
content_type = "application/octet-stream"
}
Decodes the rendered string as base64 and sends the resulting bytes.
body {
kind = "file_stream"
path = "{{ args.file_path }}"
content_type = "application/pdf"
}
Reads a file from disk and sends its contents as the request body.
The auth block lives inside an operation block and has a kind discriminator field. Every secret key referenced in auth must also appear in annotations.secrets.
auth {
kind = "bearer"
secret = "provider.token"
}
Field Required Description kindyes "bearer"secretyes Key name in the OS keychain. Value is sent as Authorization: Bearer <value>.
auth {
kind = "api_key"
secret = "provider.api_key"
location = "header" # header, query, or cookie
name = "X-Api-Key"
}
Field Required Description kindyes "api_key"secretyes Key name in the OS keychain. locationyes Where to send the key: "header", "query", or "cookie". nameyes Header name, query parameter name, or cookie name.
auth {
kind = "basic"
username = "{{ secrets.jira_email }}"
password_secret = "provider.password"
}
Field Required Description kindyes "basic"usernameyes Username string. Supports Jinja expressions, including secrets.* to pull from the keychain. password_secretyes Key name in the OS keychain whose value is the password.
auth {
kind = "o_auth2_profile"
profile = "myprofile"
}
Field Required Description kindyes "o_auth2_profile"profileyes Name of an OAuth2 profile defined in ~/.config/earl/config.toml. Earl fetches, caches, and refreshes the access token automatically.
See Secrets & Authentication for OAuth2 profile configuration.
result {
decode = "json"
extract = { json_pointer = "/items" }
output = "Found {{ result | length }} items."
result_alias = "items"
}
Field Type Default Description decodestring "auto"How to parse the response body before the output template runs. extractblock — Extract a subset of the decoded response. outputstring "{{ result }}"Jinja template rendered to produce the final output. The variable result holds the decoded (and optionally extracted) response. result_aliasstring — Rename the result variable in the output template. result_alias = "items" makes the response available as {{ items }} instead of {{ result }}.
Value Description "auto"Infers mode from Content-Type header, then falls back to JSON detection on the body. "json"Parse body as JSON. result is the decoded object or array. "text"Treat body as UTF-8 text. result is a string. "html"Treat body as HTML text. result is a string. "xml"Treat body as XML text. result is a string. "binary"Return raw bytes. result is the bytes base64-encoded as a string.
Extract runs after decode and before the output template. Use it to pull a specific value out of a large response.
# JSON Pointer (RFC 6901)
extract = { json_pointer = "/data/users" }
# Regular expression — result is the first capture group
extract = { regex = "token=([A-Za-z0-9]+)" }
# XPath — for XML responses
extract = { xpath = "//user/name/text()" }
# CSS selector — for HTML responses
extract = { css_selector = "h1.title" }
The transport block is optional and available for all protocols except SQL (where it applies at connection level). It controls low-level network behavior.
transport {
timeout_ms = 30000
max_response_bytes = 8388608
redirects {
follow = true
max_hops = 5
}
retry {
max_attempts = 3
backoff_ms = 250
retry_on_status = [ 429 , 503 ]
}
compression = true
tls {
min_version = "1.2" # 1.0, 1.1, 1.2, or 1.3
}
proxy_profile = "corporate"
}
Field Type Default Description timeout_msinteger 30000Request timeout in milliseconds. max_response_bytesinteger 8388608 (8 MiB)Maximum response body size. Clamped between 1 KiB and 128 MiB. redirects.followboolean trueWhether to follow HTTP redirects. redirects.max_hopsinteger 5Maximum redirect chain length. retry.max_attemptsinteger 1Total attempts including the first. Values below 1 are treated as 1. retry.backoff_msinteger 250Delay between retries in milliseconds. retry.retry_on_statuslist of integers []HTTP status codes that trigger a retry (e.g. [429, 503]). compressionboolean trueWhether to accept compressed responses. tls.min_versionstring — Minimum TLS version: "1.0", "1.1", "1.2", or "1.3". proxy_profilestring — Name of a proxy profile defined in ~/.config/earl/config.toml.
When an environment is active, its override replaces the command's default operation (and optionally result) entirely.
environment_overrides {
staging {
operation {
protocol = "http"
method = "GET"
url = "https://staging.example.internal/api/items"
auth {
kind = "bearer"
secret = "myapp.staging_token"
}
}
result {
decode = "json"
output = "[staging] {{ result | length }} items"
}
}
}
Each named block inside environment_overrides corresponds to an environment name defined in the provider's environments block. See Environments for how environments are configured and activated.
The result override inside an environment is optional. If omitted, the command's default result block applies.
environments {
default = "production"
secrets = [ "myapp.prod_token" ]
production {
base_url = "https://api.myapp.com"
}
staging {
base_url = "https://staging.myapp.com"
}
}
Field Type Description defaultstring Environment name that is active when none is specified. secretslist of strings Secrets resolved before environment variable values are rendered. environmentsmap Named environments. Each environment is a map of string variable names to Jinja template strings. Those variables are available in operation fields as {{ vars.base_url }} etc.
See Environments for the complete guide.