| name | adl |
| description | Author and maintain projects built with ADL (Agent Definition Language) - the YAML manifest format from `inference-gateway/adl` that `adl-cli` turns into a full A2A agent scaffold (Go/Rust/TypeScript). Use when working inside a generated agent project (presence of `agent.yaml` + `.adl-ignore`), when editing the manifest, when implementing custom tools and skills against the generated scaffold, or when planning the schema-first / domain modelling for a new agent.
|
| license | Apache-2.0 |
ADL (Agent Definition Language) Expert
Use this skill when working in any project whose root contains an agent.yaml
manifest with apiVersion: adl.inference-gateway.com/v1 (or v2+ when it
ships) together with an .adl-ignore file. That pair is the fingerprint of a
project generated by adl-cli
from a schema in inference-gateway/adl.
Also use it when the user is authoring a fresh manifest (adl init) or
planning the domain model for a new agent.
Mental model
ADL is "OpenAPI for AI agents". A single declarative manifest names everything
the agent needs - capabilities, the AI provider, services, function-call
tools, markdown skills, server/auth, language runtime, sandbox, deployment -
and adl-cli turns it into an enterprise-ready, A2A-compatible project. You
own the manifest and the bodies of TODO placeholders; the CLI owns everything
else and will regenerate it on demand.
The contract has two halves you must keep straight:
| Concept | Where it lives | Who owns it | Regenerated on adl generate? |
|---|
Manifest (agent.yaml) | repo root | you | no - source of truth |
Scaffolding (main.go, config/, internal/<service>/, Dockerfile, CI, ...) | various | the generator | yes |
Custom tool bodies (tools/<name>.{go,rs,ts}) | tools/ | you, after a TODO scaffold | no - protected by .adl-ignore |
Custom service impls (internal/<service>/*.go) | internal/ | you, after a TODO scaffold | no - protected by .adl-ignore |
Skills (.agents/skills/<id>/SKILL.md) | .agents/skills/ | you (bare) or upstream (sourced) | no - whole dir protected |
If you forget which half a file belongs to, cat .adl-ignore - anything
matched there survives adl generate --overwrite.
Spec at a glance. Every spec.* top-level field the v1 schema defines:
spec.* field | Required? | Purpose |
|---|
capabilities | yes | A2A feature flags - streaming, pushNotifications, stateTransitionHistory (all three booleans) |
server | yes | port (1-65535), optional scheme, debug, auth.enabled, authz (enabled + mode) |
language | yes | At least one of go, typescript, rust (each with its own required pair, e.g. module+version) |
agent | no | LLM provider/model/systemPrompt/maxTokens/temperature + mcp MCP client (servers + runtime config) |
card | no | Static A2A agent-card metadata + advertised security schemes (A2A section 7) |
services | no | Domain services declared as ports (type, interface, factory, description) |
config | no | Arbitrary per-section config maps; one section per service (env-mapped) |
tools | no | Function-call entrypoints - reserved built-in ids and user tools |
skills | no | Markdown playbooks - registry / GitHub source: / bare: true |
acronyms | no | String list the generator preserves in generated identifier casing |
artifacts | no | enabled: true to generate an artifacts server (filesystem or MinIO backend) |
telemetry | no | enabled: true for OpenTelemetry; / select per-signal exporters (Go/TS only) |
The sections below cover each in turn. Anything not in this table is not in
v1 - if you see it in an existing manifest, treat it as a CLI extension and
verify against adl-cli's changelog.
Schema-first / domain-first workflow
ADL is schema-driven by design: the manifest is the domain model. Sequence
work so the manifest leads and the code follows.
- Model the domain in YAML before touching any source file.
- Name the bounded context in
metadata.name (lowercase, hyphenated) and
pin metadata.version (semver - ^\d+\.\d+\.\d+$). Optional metadata:
author (name required, email/url), license (same SPDX enum as
skills, or Proprietary), and tags[] for discoverability.
- Declare the required spec frame first:
spec.capabilities (all
three booleans - streaming, pushNotifications,
stateTransitionHistory), spec.server.port, and at least one
spec.language.{go|typescript|rust} target. Almost always set
spec.agent (provider + model) and spec.card (A2A discovery) too -
they're optional in the schema but the agent is useless without them.
- Enumerate services (
spec.services.*) as ports: each gets a
type (service/repository/client/middleware), an interface,
a factory, and a description. One service per responsibility - don't
conflate database and cache.
- Enumerate config sections (
spec.config.*) as value objects. Use
dotted-name injection (config.database) to give a tool only the
subsection it needs.
- Enumerate tools (
spec.tools[]) as commands - the verbs the model
can call. Each one declares its JSON Schema (schema:) and the services
it depends on (inject:).
- Enumerate skills (
spec.skills[]) as the procedural knowledge - the
"how" and "when" for using the tools. Skills are markdown, not code.
- Validate the manifest before generating.
adl validate agent.yaml
checks shape against the pinned schema and rejects typos in reserved
namespaces (e.g. spec.config.tools.bash.tymeout_seconds).
- writes the
scaffold. The first run creates with every file containing a
TODO marked for protection. Re-run with to refresh
non-protected scaffolding after a manifest edit.
Capabilities, the LLM, and the agent card
Three top-of-spec blocks shape what the agent advertises, which model it
talks to, and how clients discover it. Set them before anything domain-
specific.
spec.capabilities (required). All three booleans must be present -
the validator rejects the manifest if any is missing:
capabilities:
streaming: true # SSE-based streaming responses
pushNotifications: false # webhook callbacks on long-running tasks
stateTransitionHistory: true # record task state transitions for replay
spec.agent (optional but near-universal). The LLM the generated agent
defers to. Provider is a fixed enum: openai, anthropic, ollama,
deepseek, google, mistral, groq, cohere, cloudflare, moonshot,
ollama_cloud, nvidia, minimax, or "" for "configure at runtime via
env vars only". Temperature is bounded to 0-2; maxTokens must be ≥1.
agent:
provider: deepseek
model: deepseek-v4-flash
systemPrompt: |
You are a helpful A2A agent. Use the AVAILABLE SKILLS playbooks for
workflows; call tools for deterministic actions.
maxTokens: 4096
temperature: 0.3
spec.agent.mcp (optional). Configuration for the ADK's built-in MCP
(Model Context Protocol) client: the servers the agent connects to at runtime
to discover and call external tools (on top of the locally generated
spec.tools), plus the global runtime settings for that client. enabled is
the required master switch (maps to A2A_MCP_ENABLED) - when false (the
default) no MCP client is generated or wired in, even if servers lists
entries. Only meaningful for an LLM-backed agent, which is why it lives under
spec.agent.
Each servers[] entry requires name (unique, ^[a-zA-Z0-9_-]+$) and
transport (stdio | sse | http): stdio launches a local subprocess
(command, args, env); http/sse connect to a remote endpoint (url,
headers). Note the Go ADK client is HTTP-only with a single shared
connection/retry set - the runtime fields below apply globally across all
servers, not per-server, and the server base URLs it dials (A2A_MCP_SERVERS)
are derived from servers:
agent:
provider: anthropic
model: claude-sonnet-5
mcp:
enabled: true # required master switch -> A2A_MCP_ENABLED
endpoint: /mcp # path appended to each server URL -> A2A_MCP_ENDPOINT
refreshInterval: 5m # tool re-discovery cadence -> A2A_MCP_REFRESH_INTERVAL
dialTimeout: 30s # connect timeout -> A2A_MCP_DIAL_TIMEOUT
callTimeout: 30s # per-call timeout -> A2A_MCP_CALL_TIMEOUT
maxRetries: 0 # 0 = retry forever -> A2A_MCP_MAX_RETRIES
retryInterval: 2s # initial backoff -> A2A_MCP_RETRY_INTERVAL
retryMaxInterval: 30s # backoff ceiling -> A2A_MCP_RETRY_MAX_INTERVAL
servers:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
- name: internal-api
transport: http
url: https://mcp.example.com/mcp
headers:
Authorization: Bearer ${MCP_TOKEN}
Every runtime field maps 1:1 to an A2A_MCP_* env var, and the manifest value
becomes the generated default (e.g. in .env.example); the env var overrides
it at runtime. enabled is the only required field - omit the rest to take the
defaults shown. (Restructured in adl v0.23.0 / adl-cli v0.54.0; the older flat
spec.agent.mcps[] list no longer validates. The env switches were renamed
from _ENABLE to _ENABLED in adl v0.24.1.)
spec.card (optional). Static fields for the A2A agent card served at
/.well-known/agent-card.json. Used by other agents and the Inference
Gateway registry for discovery. The base fields are free-form
strings/arrays:
card:
protocolVersion: "0.3.0"
preferredTransport: JSONRPC
defaultInputModes: [text, voice]
defaultOutputModes: [text, audio]
url: "https://my-agent.example.com:8443"
documentationUrl: "https://github.com/company/my-agent/docs"
iconUrl: "https://github.com/company/my-agent/icon.png"
Since adl v0.24.0 the card also carries the A2A section-7 auth surface:
supportsExtendedAgentCard: true makes the generated ADK serve the
authenticated GET /extendedAgentCard endpoint (with the A2A error
contract for unsupported/misconfigured calls). Defaults to false.
securitySchemes declares named schemes in flat OpenAPI-3.0 authoring
form - type: apiKey (plus name and in: query|header|cookie),
type: http (plus scheme, optional bearerFormat), or
type: mutualTLS. OIDC/OAuth2 are deliberately not modelled here:
they are runtime concerns (AUTH_ISSUER_URL / AUTH_CLIENT_ID /
AUTH_CLIENT_SECRET env) and the ADK derives their declaration at
startup.
security lists the advertised requirements, each entry mapping a scheme
name from securitySchemes to its required scopes (empty list for
scope-less schemes). OpenAPI semantics: keys within one entry are ANDed,
separate array entries are ORed.
card:
protocolVersion: "0.3.0"
supportsExtendedAgentCard: true
securitySchemes:
apiKey:
type: apiKey
name: X-API-Key
in: header
bearer:
type: http
scheme: Bearer
bearerFormat: JWT
security:
- apiKey: []
- bearer: []
Server, auth, and language targets
spec.server (required). Only port is mandatory. scheme (http/
https), debug, auth, and authz are optional. auth is the
authentication on/off toggle - the concrete provider (OIDC, JWT, ...) lives
in the generated code and config sections, not the schema:
server:
port: 8443
scheme: https
debug: false
auth:
enabled: true
authz:
enabled: true
mode: deny-all
spec.server.authz (adl v0.25.0). Authorization, as distinct from
authentication. enabled: true scaffolds a user-owned BeforeTool
authorization callback in the generated project; mode sets the default
policy until you implement custom logic - allow-all (the default),
deny-all, or custom (you must write the logic yourself). Both fields are
optional; omit the whole block and no authz scaffold is generated. Pair
authz with the card's securitySchemes/security block so what the agent
enforces matches what its card advertises.
spec.language (required). At least one of three children; the
generator emits the corresponding scaffold. Required pairs per language:
| Language | Required fields | Optional |
|---|
go | module, version | - |
typescript | packageName, nodeVersion | - |
rust | packageName, version, edition | features[] |
language:
go:
module: github.com/example/my-agent
version: "1.26.2"
vendor.{deps,devdeps}. Every language block accepts a vendor object
(schema-validated since v0.11): deps[] for runtime dependencies and
devdeps[] for dev/test-only tools, each entry in <package>@<version>
form using the language's native syntax. The manifest is authoritative:
since adl-cli v0.48.0 the generator rewrites go.mod / Cargo.toml /
package.json from it, so any dependency your custom code imports but does
not declare here is silently dropped on the next adl generate. Add extra
deps in the manifest - never by editing go.mod directly:
language:
go:
module: github.com/example/my-agent
version: "1.26.2"
vendor:
deps:
- github.com/stretchr/testify@v1.10.0
devdeps:
- golang.org/x/tools/cmd/stringer@v0.20.0
For Go, devdeps become tool directives - CLI executables only (e.g.
counterfeiter, stringer). Libraries imported by _test.go files (e.g.
testify) belong in deps. Pair vendor deps with a
spec.hooks.post: [go mod tidy] hook (see
Artifacts, telemetry, and post-generate hooks)
so the indirect dependency graph stays consistent after each regeneration.
Tools vs Skills (the often-confused distinction)
ADL distinguishes two complementary surfaces, and conflating them produces
unmaintainable agents. Apply the rule first, then write the entry.
Use a tool (spec.tools[]) when | Use a skill (spec.skills[]) when |
|---|
| The agent must invoke a deterministic operation (DB query, HTTP call, file write) | The agent must learn a workflow, policy, or response pattern |
| Inputs and outputs are structured (JSON Schema fits) | The instructions are prose |
| Implemented in code | Authored as markdown |
| Registered with the toolbox at startup | Loaded into the system prompt at startup via the AVAILABLE SKILLS: manifest |
A skill that needs to read files (e.g. its own SKILL.md body, or bundled
templates) requires - id: read in spec.tools and
spec.config.tools.read.enabled: true. The validator enforces this; don't
disable it.
Reserved built-in tools
spec.tools[] recognises five reserved ids that map to framework-supplied
implementations. They ship with their own unit tests (see
builtin/*_test.go.tmpl in the CLI templates), so you do not need to
write tests for them. You activate them - that's all:
| Reserved id | Purpose | Activation namespace |
|---|
read | Read a file (file_path, optional offset/limit) | spec.config.tools.read |
bash | Execute a whitelisted shell command with a timeout | spec.config.tools.bash |
write | Write content to a file (creates parent dirs) | spec.config.tools.write |
edit | Replace a unique old_string with new_string in a file | spec.config.tools.edit |
fetch | GET/HEAD an http(s) URL (host whitelist, byte cap) | spec.config.tools.fetch |
All five default to enabled: false. Opt in by listing the id alone (no
name, description, or schema - the generator owns those) and setting
enabled: true in the matching spec.config.tools.<id> block. The reserved
config block accepts only the typed keys the generator knows; typos like
tymeout_seconds fail validation by design.
Resolution precedence at runtime is env > compile-time literal > built-in
default (disabled). The kill-switch envs (A2A_BASH_DISABLED=1,
A2A_FETCH_DISABLED=1, etc.) override the compile-time enabled: true.
Each built-in accepts its own typed config keys under
spec.config.tools.<id>. Anything not on this list fails validation:
| Tool | Config keys |
|---|
read | enabled, max_lines (default file slice), allowed_roots[] (empty = project-wide) |
bash | enabled, whitelist[] (allowed commands), timeout_seconds |
write | enabled |
edit | enabled |
fetch | enabled, allowed_domains[] (entries starting with . match any subdomain), max_bytes, timeout_seconds, allow_downloads, download_dir |
A representative configuration that opts in the three tools with the
richest config surfaces:
config:
tools:
read:
enabled: true
max_lines: 2000
bash:
enabled: true
whitelist: [ls, cat, grep, find, rg, jq, wc, head, tail, git, go]
timeout_seconds: 30
fetch:
enabled: true
allowed_domains:
- pkg.go.dev
- .rust-lang.org # any subdomain of rust-lang.org
- raw.githubusercontent.com
max_bytes: 5242880 # 5 MiB
timeout_seconds: 20
allow_downloads: true
download_dir: /tmp/adl-fetch-cache
tools:
- id: read
- id: bash
- id: fetch
Custom tool implementation
A user tool is a full spec.tools[] entry - id, name, description,
optional tags, inject, and a JSON Schema for schema. The generator
produces tools/<name>.{go,rs,ts} with:
- a struct holding the injected dependencies (
logger, services, optional
config or config.<section> subsections),
- a constructor
New<PascalName>Tool(...) server.Tool whose signature mirrors
inject: in declaration order, and
- a handler method
<PascalName>Handler(ctx, args) (string, error) whose body
is a single // TODO: Implement <name> logic comment plus a placeholder
return.
You replace the handler body. Do not change:
- the struct name or its field order (regeneration of
main.go wires fields
positionally),
- the constructor signature,
- the handler method name.
Injection patterns you can use in inject::
inject:
- logger # always available, *zap.Logger (Go)
- config # the whole *config.Config
- config.database # only *config.DatabaseConfig - principle of least privilege
- cache # any name declared in spec.services
Always prefer config.<section> over config - it keeps the tool's blast
radius small, makes unit tests trivial to set up, and the validator catches
mismatches between inject: and spec.config.* at generate time.
Testing custom tools (mandatory)
The built-in reserved tools (read, bash, write, edit, fetch) ship
with generated unit tests. Custom tools do NOT. This is the single most
common gap in ADL projects.
Verification step for every PR that adds or modifies spec.tools[]:
- For each entry whose
id is not in the reserved set above, confirm a
sibling test file exists - tools/<name>_test.go (Go), tools/<name>.rs
tests block (Rust), tools/<name>.test.ts (TypeScript) - and the file is
listed in .adl-ignore so regeneration won't clobber it.
- The test file MUST exercise the handler against a service mock for every
dependency in
inject:. Use the service's interface (declared in
spec.services.*.interface) - that's why it exists.
- Use table-driven tests (Go) / parameterised tests (Rust) /
describe+it
blocks (TypeScript) covering at least: happy path, invalid args, service
error.
- Run the language-native test command from the generated Taskfile:
task test (Go and Rust) or the equivalent target. CI must pass.
A minimal Go template for a custom tool test (adapt to the actual struct
name and interface):
package tools
import (
"context"
"errors"
"testing"
"go.uber.org/zap"
"github.com/stretchr/testify/require"
)
type stubDatabase struct{ err error; out string }
func (s *stubDatabase) Query(ctx context.Context, sql string) (string, error) {
return s.out, s.err
}
func TestQueryDatabaseTool_Handler(t *testing.T) {
cases := []struct {
name string
args map[string]any
db *stubDatabase
wantErr bool
}{
{"happy path", map[string]any{"query": "SELECT 1", "table": "t"}, &stubDatabase{out: `{"rows":1}`}, false},
{"missing query", map[string]any{"table": "t"}, &stubDatabase{}, true},
{"backend error", map[string]any{"query": "SELECT 1", "table": "t"}, &stubDatabase{err: errors.New("boom")}, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tool := &QueryDatabaseTool{logger: zap.NewNop(), database: tc.db}
_, err := tool.QueryDatabaseHandler(context.Background(), tc.args)
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
Skip this and the agent ships untested business logic - the schema can't
catch a misread args["query"].(string) or a swallowed service error.
The .adl-ignore contract
adl-cli writes this file on first generation. It works like .gitignore
but applies to the generator - anything matched is preserved across
adl generate --overwrite. The generator automatically adds:
- every custom tool file (
tools/<name>.<ext>),
- every custom service implementation (
internal/<service>/*),
- every generated skill directory (
.agents/skills/<id>/).
You can extend it to protect anything else you've hand-edited:
# .adl-ignore
Dockerfile # custom container build
k8s/ # using ArgoCD overlays elsewhere
Taskfile.yml # extended with project-specific targets
README.md # written by hand
Patterns: # for comments, trailing / for directories, * wildcards,
exact paths.
When a regeneration goes wrong: check .adl-ignore first. A missing
entry there is the usual cause of "my custom code disappeared after I edited
the manifest."
Services and configuration
Services are first-class - declare them in spec.services with type,
interface, factory, and description, and the generator creates
internal/<service>/<service>.{go,rs} with an interface stub and a factory
that takes (*zap.Logger, *config.Config).
type is a closed enum - pick the one that best names the role so the
generator can emit idiomatic scaffolding:
type | Use it for |
|---|
service | Domain logic / orchestration (default choice when in doubt) |
repository | Persistence and data-access ports - DBs, object stores |
client | Outbound HTTP / gRPC / RPC clients to another system |
middleware | Request-pipeline behaviour - auth, logging, rate limits |
interface and factory must match ^[a-zA-Z][a-zA-Z0-9_]*$ - they
become real identifiers in the generated code. Three rules keep this
maintainable:
- One interface per responsibility. Don't ship a
UtilService. Split
database and cache even if they share a backend.
- Inject the interface, never the implementation. Tools depend on
database.DatabaseService, not on *database.databaseService. This is
what makes the unit-test stubs above trivial.
- Configuration mirrors services. A service named
googleCalendar
reads its settings from spec.config.googleCalendar -> generated as
GoogleCalendarConfig with GOOGLE_CALENDAR_* env prefix. Don't
hand-wire env vars; let the prefix mapping do it.
Skills inside an ADL agent
spec.skills[] accepts three entry shapes, resolved by adl-cli:
| Shape | Behaviour |
|---|
id: <name> (and optional version: <semver>) | Fetched from https://registry.inference-gateway.com/skills/. Override with ADL_SKILLS_REGISTRY. |
id: <name> + source: <shorthand-or-URL> | The whole GitHub directory (SKILL.md + any bundled assets) is pulled into .agents/skills/<id>/. |
id: <name> + bare: true (+ name, description, tags, optional license) | Scaffolded locally as a TODO. Author it by hand. |
source: shorthand:
- id: skill-creator
source: skill-creator # inference-gateway/skills, main
- id: skill-creator
source: skill-creator@v1.0 # pinned tag
- id: pdf
source: anthropics/skills/pdf # different repo
- id: pdf
source: anthropics/skills/pdf@abc1234 # pinned commit SHA
- id: custom
source: https://github.com/my-org/my-repo/tree/release/path/to/skill
At runtime, the generated agent walks first-level subdirectories under
.agents/skills/ (override with A2A_SKILLS_DIR), parses each
<id>/SKILL.md's frontmatter, and appends an AVAILABLE SKILLS: block to the
system prompt - the bodies are not inlined. The model loads them on demand
via the read tool, so a skills-using agent must opt read in (see "Reserved
built-in tools"). The generator also symlinks .claude/skills -> ../.agents/skills, so Claude Code reads the same tree at
.claude/skills/<id>/SKILL.md. (adl-cli v0.52.2 moved generated skills from
skills/ to .agents/skills/; existing projects must move the directory - or
set A2A_SKILLS_DIR=skills - on the next regenerate.)
license: on a skill entry must be one of the SPDX identifiers the schema
accepts (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, GPL-2.0,
GPL-3.0, LGPL-2.1, LGPL-3.0, MPL-2.0, ISC, CC0-1.0,
CC-BY-4.0, CC-BY-SA-4.0, Unlicense) or the literal Proprietary. SPDX
expressions like MIT OR Apache-2.0 are not currently accepted.
Local development: sandbox and AI assistants
spec.development configures the experience of working on the agent
locally - reproducible dev environments and AI-assistant onboarding files.
Three independent subsections (sandbox, ai, deps), all optional.
spec.development.sandbox. Three alternative packagings - pick any
combination; each declares its own enabled boolean. Generated artefacts:
| Sub-block | enabled: true generates |
|---|
flox | .flox/env/manifest.toml (Flox/Nix-backed reproducible env) |
devcontainer | .devcontainer/devcontainer.json (VS Code Dev Containers) |
dockerCompose | docker-compose.yaml (with the artifacts server wired in when spec.artifacts.enabled: true) |
spec.development.ai.orchestrators. Per-agent toggles for the coding
assistants the project is meant to be edited with, nested under
ai.orchestrators (the older flat ai.<agent> and ai.enabled shapes are
rejected by adl validate/adl generate with a migration hint - move
the toggle under orchestrators). Each is independent; all default off.
Enabling one generates its onboarding doc plus a GitHub Actions workflow,
kept in sync on adl generate --overwrite unless listed in .adl-ignore:
| Sub-block | Generated docs file | Generated workflow |
|---|
claudecode | CLAUDE.md (Anthropic Claude Code) | .github/workflows/claude.yml |
gemini | GEMINI.md (Google Gemini) | .github/workflows/gemini.yml |
codex | shared AGENTS.md (OpenAI Codex) | .github/workflows/codex.yml |
opencode | shared AGENTS.md | none (no upstream action yet) |
infer | shared AGENTS.md (Inference Gateway infer) | .github/workflows/infer.yml |
Enabling claudecode also provisions the claude-code CLI into the Flox /
DevContainer sandboxes automatically.
A small example combining sandbox + AI toggles:
development:
sandbox:
flox:
enabled: true
devcontainer:
enabled: false
dockerCompose:
enabled: true
ai:
orchestrators:
claudecode:
enabled: true
codex:
enabled: true
spec.development.deps[]. A cross-cutting list of sandbox-level tool
dependencies (<package>@<version>, e.g. kubectl@1.31.0) installed into
every enabled sandbox flavour - for tools that don't belong to any single
language's package manager.
SCM, CI/CD, and deployment
Two related blocks that together drive everything outside main.go: where
the code lives, how it ships, and where it runs.
spec.scm. All fields optional; the provider enum is closed:
| Field | Effect |
|---|
provider | github | gitlab | bitbucket - selects the workflow templates |
url | Repository URL (used in generated README.md, agent card, etc.) |
github_app | Generate a GitHub App / token configuration in CI |
issue_templates | Write .github/ISSUE_TEMPLATE/*.md |
dependabot | Write .github/dependabot.yml |
ci | Write .github/workflows/ci.yml |
cd | Write .github/workflows/cd.yml and .releaserc.yaml (semantic-release) |
When github_app: true, the generated CD workflow reads the App credentials
from repo secrets RELEASER_APP_ID / RELEASER_APP_PRIVATE_KEY by default;
override the names with spec.scm.app_id_secret / app_private_key_secret.
The claudecode and infer orchestrators take the same override pair -
appIdSecret / appPrivateKeySecret, defaulting to CLAUDE_APP_* /
INFER_APP_* (see spec.development.ai.orchestrators).
spec.deployment. Choose type: kubernetes, cloudrun, vercel, or
cloudflare; the matching sub-block carries the detail. kubernetes and
cloudrun deploy a prebuilt container image and share an image shape
(registry, repository, tag, optional useCloudBuild); vercel and
cloudflare deploy from source via the platform's own build pipeline, so
they have no image block. In any environment: map, use ${VAR}
placeholders for secrets - never inline real values.
type: kubernetes generates k8s/deployment.yaml:
deployment:
type: kubernetes
kubernetes:
image:
registry: ghcr.io
repository: example/my-agent
tag: v1.0.0
type: cloudrun generates cloudrun/ helpers plus a deploy target in
the Taskfile.yml. Fields map 1:1 to Cloud Run concepts:
deployment:
type: cloudrun
cloudrun:
image:
registry: gcr.io
repository: my-agent
tag: v1.0.0
useCloudBuild: true
resources:
cpu: "1"
memory: 512Mi
scaling:
minInstances: 0
maxInstances: 100
concurrency: 1000
service:
timeout: 3600
allowUnauthenticated: true
serviceAccount: my-agent@PROJECT_ID.iam.gserviceaccount.com
executionEnvironment: gen2
environment:
LOG_LEVEL: info
ENVIRONMENT: production
type: vercel deploys from source through Vercel's build pipeline. Fields:
project, team, framework (omit to auto-detect), runtime (nodejs |
edge), regions[], functions.{memory,maxDuration}, environment:
deployment:
type: vercel
vercel:
project: my-agent
runtime: nodejs
regions: [iad1]
functions:
memory: 1024
maxDuration: 300
environment:
LOG_LEVEL: info
type: cloudflare targets Cloudflare Workers (not Pages); the CLI
translates the block into wrangler configuration. Fields: name,
accountId (prefer a ${VAR} placeholder), compatibilityDate
(YYYY-MM-DD; generator supplies a default if omitted),
compatibilityFlags[] (e.g. nodejs_compat), routes[], workersDev,
environment (wrangler vars; real secrets go out-of-band via
wrangler secret put):
deployment:
type: cloudflare
cloudflare:
name: my-agent
accountId: ${CLOUDFLARE_ACCOUNT_ID}
compatibilityDate: "2026-01-01"
compatibilityFlags: [nodejs_compat]
routes:
- agent.example.com/*
workersDev: false
adl generate exposes equivalent CLI flags (--ci, --cd,
--deployment kubernetes|cloudrun, --flox, --devcontainer) that OR
with the manifest values. The --deployment flag only accepts
kubernetes and cloudrun - vercel and cloudflare are manifest-only. Prefer the manifest for anything that needs to
be reproducible across machines and CI runs - treat the flags as
one-off escape hatches.
Documentation pages and examples
The generator owns two root-level docs, regenerated on every run: README.md
(overview, quick start, tools/skills/examples tables) and CONFIGURATIONS.md
(the full config reference - the custom spec.config table plus every A2A_*
env var, including the telemetry rows when enabled). The README's
Configuration section is just a short paragraph linking to CONFIGURATIONS.md.
Neither is in .adl-ignore by default; add them yourself if you fork them.
Two spec blocks enrich the generated README with hand-authored content.
spec.documentation.pages[] (optional). Declare hand-authored documentation
pages that link from the generated README. Each entry requires title and path
(the path relative to the repo root, typically docs/<file>.md); description
is optional. The generator creates a stub docs/<file>.md on first run (title-only,
never overwrites) and renders a ## Documentation section in the README:
documentation:
pages:
- title: Getting Started
path: docs/getting-started.md
description: Quickstart guide for the agent
- title: Architecture
path: docs/architecture.md
description: System design and component overview
The stub files follow the same seed-once pattern as bare skill scaffolds - the
generator writes them only if they do not exist, so your edits survive
adl generate --overwrite. Add the docs/ directory to .adl-ignore if you
want full control over the file set.
spec.examples[] (optional). Declare curated examples that link from the
generated README. Each entry has title and description only - there is no
path field. The generator derives a directory from the title
(lowercased, spaces to dashes) and seeds examples/<slug>/README.md once
(title + description + TODO, never overwritten); the README's ## Examples
table links each entry to its directory. The examples/ directory is listed
in .adl-ignore, so everything you add there survives regeneration:
examples:
- title: Basic Chat # -> examples/basic-chat/
description: A simple request-response interaction
- title: Multi-turn Workflow # -> examples/multi-turn-workflow/
description: Chaining several tool calls across turns
Both blocks are purely additive: omitting them produces the same README as
before.
Artifacts, telemetry, and post-generate hooks
Three small spec blocks that round out the manifest.
spec.artifacts.enabled. Set true to generate an artifacts server -
a small HTTP service for storing task outputs - and to wire the matching
create_artifact / read_artifact / list_artifacts helpers into the
generated code. The backend (filesystem vs MinIO) is chosen at runtime via
env vars on the generated binary, not via the manifest. When
spec.development.sandbox.dockerCompose.enabled: true, the MinIO instance
is wired into the generated docker-compose.yaml.
artifacts:
enabled: true
spec.telemetry.enabled. Set true to pull OpenTelemetry dependencies
into the project, instrument built-in tool calls with spans, and turn on the
ADK's telemetry/metrics server (the A2A_TELEMETRY_ENABLED switch). Disabled
by default. Supported for Go and TypeScript targets only - Rust manifests
ignore the block.
telemetry:
enabled: true
enabled is the master switch and the only required field. The optional
traces and metrics blocks each select a per-signal exporter, following
the OpenTelemetry SDK declarative-configuration model: the exporter is nested
under the signal and the single key beneath exporter picks it - otlp
(push) for either signal, prometheus (pull) for metrics only. There is no
separate exporter enum, and exactly one exporter key is allowed per signal.
telemetry:
enabled: true
traces:
exporter:
otlp:
endpoint: http://localhost:4318 # -> A2A_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
protocol: http/protobuf # http/protobuf | grpc
metrics:
exporter:
prometheus: # pull; use `otlp:` to push instead
host: ""
port: 9464
Every field maps 1:1 to a standard OTEL_* environment variable carried
under the A2A_ prefix for all languages (A2A_OTEL_*), which adl-cli
emits as a generated .env.example default. Generated TypeScript agents
mirror A2A_OTEL_* onto the bare OTEL_* names the Node SDK reads at
startup; explicitly set bare names win. Omitting a signal (or its exporter
block) disables it - A2A_OTEL_TRACES_EXPORTER=none /
A2A_OTEL_METRICS_EXPORTER=none. Headers, credentials, and sampling deliberately
stay out of the manifest and are resolved at runtime through the environment.
traces/metrics are purely additive, so an existing
telemetry: { enabled: true } manifest stays valid.
spec.hooks.post. A list of shell commands the CLI runs at the end of
every adl generate. Use for the things a generator can't reasonably
infer:
hooks:
post:
- go mod tidy
- go generate ./...
Common commands
adl init is for bootstrapping a fresh agent.yaml; adl validate /
adl generate are the steady-state loop. Run from the repo root:
adl init [name] # interactive wizard for a new agent.yaml
adl init [name] --defaults # non-interactive; accept the defaults
adl validate [file] # shape check; default file is agent.yaml
adl generate -f agent.yaml -o . # initial scaffold
adl generate -f agent.yaml -o . --overwrite # refresh after manifest edit (respects .adl-ignore)
adl generate ... --offline # skip skills registry; require local cache (~/.adl/skills-cache/)
adl generate ... --ci # also write .github/workflows/ci.yml
adl generate ... --cd # also write cd.yml + .releaserc.yaml
adl generate ... --deployment kubernetes # also write k8s/deployment.yaml
adl generate ... --deployment cloudrun # also write cloudrun/ helpers + Taskfile deploy target
adl generate ... --flox # enable Flox sandbox (OR with spec.development.sandbox.flox.enabled)
adl generate ... --devcontainer # enable DevContainer sandbox (OR with spec.development.sandbox.devcontainer.enabled)
adl generate ... -t minimal # template selector (currently only "minimal" ships)
In a generated project, prefer the repo tasks over invoking adl
directly. A globally installed adl may be stale or unapproved in a
sandboxed/CI session; the project pins the correct version itself.
Generated projects ship task validate (adl validate agent.yaml) and
task generate (adl generate --overwrite) in their Taskfile.yml, and
when a .flox/ env exists it pins the adl-cli version
(.flox/env/manifest.toml) - run through it:
flox activate -- task validate # adl validate agent.yaml, pinned CLI
flox activate -- task generate # adl generate --overwrite, pinned CLI
After task generate, check git status - anything custom that changed
means an .adl-ignore gap. To pick up a schema change, bump the adl-cli
flake ref in .flox/env/manifest.toml rather than upgrading a global
binary.
Once generated:
task build # compile
task test # run unit tests - INCLUDING custom tool tests
task run # start the agent on spec.server.port
task deploy # only present when spec.deployment is set
Validate the manifest in CI on every PR. Add a job step that runs
adl validate agent.yaml before any code build - it's faster and surfaces
schema drift early. Keep the manifest as the source of truth - prefer
spec.scm.ci: true over --ci, spec.deployment.type: cloudrun over
--deployment cloudrun, and so on. The flags exist for one-off enabling;
they are not a substitute for declarative state.
Cross-repo awareness
ADL touches several repos in the inference-gateway org. When a change in
one of them might ripple, surface it explicitly:
inference-gateway/adl (schema; this skill lives here) -> consumers must
bump the pinned tag in their Taskfile.yml/internal/schema/. Breaking
changes go to v2/, not on top of v1/.
inference-gateway/adl-cli (generator) -> template edits land here, not
in the schema. A generator change without a schema change means existing
manifests keep working.
inference-gateway/skills (the skills catalog) -> if a spec.skills[]
entry uses source: <id> without an owner, it resolves here. Pin via
@<tag> for reproducibility.
For ecosystem-wide concerns (release flow, conventional commits,
cross-repo checklists), consult inference-gateway/.github (org-level
CLAUDE.md / README.md) and each repo's own CLAUDE.md.
What NOT to do
- Don't hand-edit generated files that are not in
.adl-ignore - they're
overwritten on the next adl generate --overwrite. If you need them
custom, add them to .adl-ignore (and accept that you've forked them
from the generator).
- Don't rename a custom tool's struct, constructor, or handler. Rename via
the manifest (
spec.tools[].name) and regenerate.
- Don't duplicate a reserved built-in id (
read, bash, write, edit,
fetch) as a custom tool. The generator owns those.
- Don't inject
config when you only need one section. Use
config.<section> - smaller blast radius, simpler tests.
- Don't make schema-breaking changes in
schema/v1/. They go to a new
major (schema/v2/) per the additive contract.
- Don't ship a custom tool without a
<name>_test.go (or language
equivalent). The reserved built-ins are tested upstream; yours are not.
- Don't conflate ADL with ADK. ADL is the manifest format; the ADK
(
inference-gateway/adk) is the Go runtime the generated main.go
imports.
- Don't omit
spec.capabilities. All three booleans (streaming,
pushNotifications, stateTransitionHistory) are required by the v1
schema; adl validate rejects the manifest if any are missing.
- Don't conflate
auth with authz. spec.server.auth toggles
authentication (who you are); spec.server.authz scaffolds the
authorization callback (what you may do) with its allow-all /
deny-all / custom default policy.
- Don't hand-edit generated
CLAUDE.md, AGENTS.md, or GEMINI.md. They're
re-emitted by the spec.development.ai.orchestrators.* generators on every
adl generate --overwrite unless you add them to .adl-ignore. If you
want the AI-onboarding docs to stay hand-written, ignore the file and
accept that you've forked it from the generator.