Skills: Packaging Agent Capabilities as Reusable, Discoverable Bundles

How ADK skills bundle instructions, tools, and resources into folders an agent can browse and load on demand.

By now in this series you’ve built agents whose knowledge lives in code: instructions in a string, tools as functions, reference data hard-wired into prompts. That works until you want the same capability in three different agents, or your instructions grow to thousands of words that are only sometimes relevant. Skills solve both problems by packaging a capability as a self-contained directory that an agent can discover, evaluate cheaply, and pull in only when it’s actually needed.

What a skill is

A skill is a folder with a SKILL.md file — YAML frontmatter (at minimum name and description) followed by a Markdown body of instructions — plus optional bundled resources (reference docs, assets, scripts). That’s it. The capability is a file tree, not code, which makes it inspectable, versionable, and portable across projects: drop the folder into a shared library and any agent pointed at that library can use it.

weather/
  SKILL.md                 # frontmatter + instruction body
  references/
    cities.txt             # a bundled resource

The SKILL.md itself:

---
name: weather                          # must equal the directory name
description: Look up the weather for a city.
license: Apache-2.0                    # optional
metadata:                              # optional key/value tags
  category: demo
---
# Weather Skill

Use this skill to answer questions about the weather in a city.

## Steps
1. Read the supported cities from `references/cities.txt`.
2. If the requested city is listed, report its weather.
3. Otherwise, tell the user the city is not supported.

The key idea: cheap discovery, on-demand load

The whole design turns on a two-step shape. Every skill’s description is loaded up front so the model can survey what’s available and choose. But the skill’s full body and resources are read only for the one it picks. This keeps the prompt small even when the library holds dozens of skills — you pay for a one-line summary per skill, not the full instruction text of every skill you might use.

flowchart TB
    subgraph Lib["skills/ (a library)"]
        W["weather/<br/>SKILL.md + references/"]
        G["greeter/<br/>SKILL.md"]
    end
    Disc["Discover:<br/>read frontmatter only"] -->|scan subdirs| Lib
    Disc --> Choice["Agent reads descriptions,<br/>picks one"]
    Choice --> Load["Load one skill in full:<br/>frontmatter + body + resources"]
    Load --> W

Discover and load in Python

ADK’s Python skills API is pure filesystem work — no model, no network. list_skills_in_dir scans the immediate subdirectories of a folder and returns a map of name to parsed frontmatter. load_skill_from_dir reads one skill in full.

from pathlib import Path
from google.adk.skills import Skill, Frontmatter, list_skills_in_dir, load_skill_from_dir

SKILLS_DIR = Path("skills")

# 1. Discover — cheap: read only each skill's frontmatter.
catalog: dict[str, Frontmatter] = list_skills_in_dir(SKILLS_DIR)
for name, fm in sorted(catalog.items()):
    print(f"{name}: {fm.description}")

# 2. Load one skill in full — instructions + bundled resources.
skill: Skill = load_skill_from_dir(SKILLS_DIR / "weather")
print(skill.frontmatter.name, skill.frontmatter.license)
print(skill.instructions.splitlines()[0])       # the Markdown body
print(sorted(skill.resources.references))        # e.g. ['cities.txt']

A loaded Skill is a single value with everything eagerly read: frontmatter (metadata), instructions (the Markdown body), and resources — dicts of references / assets / scripts keyed by filename. You pass it around as one object.

The same two operations in Go

Go models a skill library as a Source — an interface you read piece by piece, so you fetch exactly the part you need when you need it. NewFileSystemSource accepts any fs.FS, which means an embed.FS works as well as os.DirFS("skills").

import (
    "context"
    "google.golang.org/adk/v2/tool/skilltoolset/skill"
)

ctx := context.Background()
src := skill.NewFileSystemSource(os.DirFS("skills"))

// 1. Discover — frontmatter only.
fms, _ := src.ListFrontmatters(ctx)
for _, fm := range fms {
    fmt.Printf("%s: %s\n", fm.Name, fm.Description)
}

// 2. Load one skill in full — read each part explicitly.
fm, _ := src.LoadFrontmatter(ctx, "weather")
instructions, _ := src.LoadInstructions(ctx, "weather")
resources, _ := src.ListResources(ctx, "weather", ".")   // ["references/cities.txt"]

// 3. Read a bundled resource on demand.
rc, _ := src.LoadResource(ctx, "weather", "references/cities.txt")
defer rc.Close()
data, _ := io.ReadAll(rc)

The contrast is idiomatic. Python hands you one fully-populated Skill object; Go hands you a Source with LoadFrontmatter, LoadInstructions, ListResources, and LoadResource, so nothing is read until you ask. If you want Python-style eager loading in Go, wrap the source with WithFrontmatterPreloadSource or WithCompletePreloadSource.

Two small asymmetries worth knowing. Resource keys differ: Python’s resources.references is keyed relative to the references/ directory ("cities.txt"), while Go’s ListResources returns paths relative to the skill root ("references/cities.txt") — same file, different prefix. And Go’s YAML parser rejects unknown top-level frontmatter keys and represents allowed-tools as a list rather than a string, so if you want one SKILL.md to load cleanly in both languages, stick to the documented fields.

Attaching skills to a live agent

Discovery and loading are the deterministic core, but to let an agent actually use a library, both SDKs expose skills as a toolset. In Go you turn a skill.Source into a skilltoolset and hand it to an llmagent:

import (
    "google.golang.org/adk/v2/tool/skilltoolset"
    "google.golang.org/adk/v2/tool/skilltoolset/skill"
)

src := skill.NewFileSystemSource(os.DirFS("skills"))
ts, _ := skilltoolset.New(ctx, skilltoolset.Config{Source: src})
// llmagent.New(llmagent.Config{..., Toolsets: []tool.Toolset{ts}})

Python backs the same lookup with a skill registry that you expose to an LlmAgent. Either way, the agent sees the frontmatter descriptions as choices and loads the full body only when it selects one.

When to reach for a skill

Mental model: a function tool (from earlier in this series) is a single deterministic call the model invokes; a skill is a bundle of instructions and supporting files the model reads and reasons over. Prefer a plain tool when the capability is one clean operation. Reach for a skill when:

Both SDKs validate that the directory name matches the frontmatter name and enforce a lowercase-hyphenated name format; a malformed SKILL.md is a load-time error, not a silent skip. That strictness is what lets a folder-of-folders behave like a trustworthy capability catalog.

Next in the series: Code Execution — letting an agent run code as a tool.

Sources & References

Skills and reusable capabilities
The Go SDK (google.golang.org/adk/v2)