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; traces/metrics select per-signal exporters (Go/TS only) |
hooks |
no | post: [...] commands the CLI runs after each adl generate |
scm |
no | provider, url, github_app, issue_templates, dependabot, ci, cd |
documentation |
no | pages[] with title, path, and optional description - hand-authored docs seeded in docs/ |
examples |
no | title and description - seeded once as examples/<slug>/README.md, linked from the README |
development |
no | sandbox.{flox,devcontainer,dockerCompose} + ai.orchestrators.{claudecode,...} + deps[] |
deployment |
no | type: kubernetes | cloudrun | vercel | cloudflare plus the matching block |
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 pinmetadata.version(semver -^\d+\.\d+\.\d+$). Optional metadata:author(namerequired,email/url),license(same SPDX enum as skills, orProprietary), andtags[]for discoverability. - Declare the required spec frame first:
spec.capabilities(all three booleans -streaming,pushNotifications,stateTransitionHistory),spec.server.port, and at least onespec.language.{go|typescript|rust}target. Almost always setspec.agent(provider + model) andspec.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 atype(service/repository/client/middleware), aninterface, afactory, and a description. One service per responsibility - don't conflatedatabaseandcache. - 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.
- Name the bounded context in
- Validate the manifest before generating.
adl validate agent.yamlchecks shape against the pinned schema and rejects typos in reserved namespaces (e.g.spec.config.tools.bash.tymeout_seconds). - Generate.
adl generate --file agent.yaml --output .writes the scaffold. The first run creates.adl-ignorewith every file containing a TODO marked for protection. Re-run with--overwriteto refresh non-protected scaffolding after a manifest edit. - Implement TODO bodies. Each generated
tools/<name>.{go,rs,ts}and eachinternal/<service>/*.{go,rs,ts}ships with// TODO:markers spelling out what to replace. The factory signature and the handler signature are locked - don't rename them, or you'll desync from the generator's expectations on the next regeneration ofmain.go. - Test what you implemented (see "Testing custom tools" - this is the non-negotiable part).
- Run.
task build && task run(Go/Rust) or the equivalent in the chosen language. The generatedTaskfile.ymlis the entry point.
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: truemakes the generated ADK serve the authenticatedGET /extendedAgentCardendpoint (with the A2A error contract for unsupported/misconfigured calls). Defaults to false.securitySchemesdeclares named schemes in flat OpenAPI-3.0 authoring form -type: apiKey(plusnameandin: query|header|cookie),type: http(plusscheme, optionalbearerFormat), ortype: mutualTLS. OIDC/OAuth2 are deliberately not modelled here: they are runtime concerns (AUTH_ISSUER_URL/AUTH_CLIENT_ID/AUTH_CLIENT_SECRETenv) and the ADK derives their declaration at startup.securitylists the advertised requirements, each entry mapping a scheme name fromsecuritySchemesto 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.7"
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.7"
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, optionalconfigorconfig.<section>subsections), - a constructor
New<PascalName>Tool(...) server.Toolwhose signature mirrorsinject:in declaration order, and - a handler method
<PascalName>Handler(ctx, args) (string, error)whose body is a single// TODO: Implement <name> logiccomment plus a placeholder return.
You replace the handler body. Do not change:
- the struct name or its field order (regeneration of
main.gowires 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
idis not in the reserved set above, confirm a sibling test file exists -tools/<name>_test.go(Go),tools/<name>.rstests block (Rust),tools/<name>.test.ts(TypeScript) - and the file is listed in.adl-ignoreso 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 inspec.services.*.interface) - that's why it exists. - Use table-driven tests (Go) / parameterised tests (Rust) /
describe+itblocks (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. Splitdatabaseandcacheeven 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
googleCalendarreads its settings fromspec.config.googleCalendar-> generated asGoogleCalendarConfigwithGOOGLE_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
…(truncated)