cd /news/ai-agents/writing-ai-agents-in-yaml-instead-of… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-159192] src=codecut.ai β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Writing AI agents in YAML instead of Python

Docker released docker-agent, an open-source tool that lets developers define AI agents entirely in a single YAML file rather than Python code, and it ships with the `docker agent` command in Docker Desktop 4.63 and later or via `brew install docker-agent`. The tool provides 30 built-in toolsets, including `rag`, `shell`, `filesystem`, `fetch`, `mcp` and `memory`, and its `rag` toolset handles indexing and vector storage automatically, storing embeddings in a local SQLite file and re-indexing changed files. In the walkthrough, an agent configured with `openai/gpt-5-mini` and `openai/text-embedding-3-small` at 1536 vector dimensions searches 72 Polars user-guide markdown files and is run with `docker agent run --exec embeddings.yaml`.

by read14 min views4 publishedOct 28, 2026
Writing AI agents in YAML instead of Python
Image: Codecut (auto-discovered)

Table of Contents #

Introduction #

There are plenty of frameworks for building AI agents, such as LangChain or LlamaIndex. But as you add models, tools, and features, the agent quickly grows into a lot of code to maintain. More code means more places for mistakes when you need to make a change.

docker-agent keeps the entire workflow in one YAML file. With this approach, you can mix and match models, tools, and features, and see how the whole agent is set up at a glance.

In this article, you will learn nice features of docker-agent by building an agent that answers questions about Polars.

πŸ’» Get the Code: The agent configs and eval files are on GitHub.

What Is docker-agent? #

docker-agent is Docker’s open-source tool for building and running AI agents.

docker-agent does for AI agents what Docker did for apps. Instead of setting up an environment and writing code, you describe the agent in one YAML file and run it with one command.

Stay Current with CodeCut

Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

Setup #

Install docker-agent with Homebrew, or use the docker agent command that ships with Docker Desktop 4.63 and later:

brew install docker-agent

The agent will search the Polars user guide and answer questions about it.

The project folder looks like this:

docker-agent-docs-qa/
└── docs/polars/    # 72 markdown files the agent will search

Create the Agent #

To create the agent, define its model, instructions, and tools in a YAML file called embeddings.yaml. Start with the agents section.

agents:
  root:    # the agent that receives your message
    model: openai/gpt-5-mini
    description: Answers questions about Polars from the user guide
    instruction: |
      You answer questions about the Polars DataFrame library.
      Always search the knowledge base before answering.
      Answer only from what the search returns.
      End with a "Sources:" list of the file paths you used, relative to docs/polars.

To let the agent search the docs, give it a rag tool and tell the tool which folder to search.

agents:
  root:
    ...
    toolsets:
      - type: rag
        ref: polars_docs          # use the source defined below

rag:
  polars_docs:                    # the name the ref points to
    tool:
      description: The Polars user guide
    docs: [./docs/polars]         # what to search
    strategies:                   # how to search
      - type: chunked-embeddings  # split each file into chunks and embed them
        embedding_model: openai/text-embedding-3-small
        vector_dimensions: 1536   # number of values per vector

RAG setups usually need a vector database and a separate indexing step. docker-agent handles both for you. It stores the vectors in a local SQLite file, indexes the folder the first time the agent starts, and re-indexes files when they change.

rag is one of 30 built-in toolsets. Here are a few others that are useful:

  • shell : run shell commands, such as a Python script orduckdb
  • filesystem : read, write, and search files in a folder
  • fetch : read web pages and online docs
  • mcp : call external services like GitHub or adatabase through MCP
  • memory : remember facts across sessions

After the YAML file is created, ask the agent a question. The --exec flag runs the agent without opening the interactive terminal UI.

docker agent run --exec embeddings.yaml "How do I read a Parquet file without  it all into memory?"

The output shows the search the agent made, followed by its answer. The search results and the model’s reasoning summary are trimmed here:

Calling polars_docs(query: "read Parquet without  all into memory scan_parquet
scan_parquet streaming lazy read_parquet memory efficient 'scan_parquet' 'read_parquet'
'scan' 'Parquet'")

Use a lazy scan and streaming collect so Polars parses and processes the file in
batches instead of materializing everything in RAM.

- Use pl.scan_parquet(...) (returns a LazyFrame) instead of pl.read_parquet(...).
  Scanning delays parsing and enables column/row pruning via the optimizer
  (projection & predicate pushdown).
- Apply selects/filters on the LazyFrame so the reader can skip unnecessary
  columns/rows.
- When collecting, use streaming execution to process in batches:
  .collect(engine="streaming").

Sources:
- io/parquet.md
- lazy/sources_sinks.md
- lazy/execution.md

The answer is correct, and every claim traces back to a file you can open.

To ask another question, run the same command with a new question without changing the YAML file.

docker agent run --exec embeddings.yaml "When should I use join_asof?"

Swap the Model #

To try a cheaper model without committing to it, you can simply pass --model for a single run.

 embeddings.yaml          --model           what runs
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ model: gpt-5-mini β”‚ ── gpt-5-nano ──>   β”‚ model: gpt-5-nano β”‚
β”‚ instruction: ...  β”‚                     β”‚ instruction: ...  β”‚
β”‚ toolsets: rag     β”‚                     β”‚ toolsets: rag     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     unchanged                               this run only

For example, this runs the same question on gpt-5-nano.

docker agent run --exec embeddings.yaml --model openai/gpt-5-nano \
  "How do I read a Parquet file without  it all into memory?"
- Use scan_parquet to read lazily. This returns a LazyFrame and does not parse the
  data into memory until you collect.
...
Sources:
io/parquet.md

This is very nice for several reasons:

  • You can quickly try different models and compare their answers
  • Because the YAML file does not change during the experiment, your teammates are not affected
  • Once you like a model, you can edit the YAML file to use it permanently

Reuse Instructions Across Questions #

You might want to vary the instructions based on the context, such as the audience, the length, or the tone.

With docker-agent, you can write different instructions in separate files, then pick one with the --prompt-file flag.

For example, you can create a pandas-users.md file with the instructions for pandas users and a sql-users.md file with the instructions for SQL users.

 "How do I read a Parquet file without  it all into memory?"
                               β”‚
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β–Ό                                       β–Ό
 --prompt-file pandas-users.md          --prompt-file sql-users.md
           β”‚                                       β”‚
           β–Ό                                       β–Ό
 Pandas: read_parquet() loads          SQL: SELECT * FROM
 the file into memory                  read_parquet('data.parquet')

Let’s try the pandas-users.md file first. Start by creating the file.

The reader knows pandas. Show the pandas equivalent next to each Polars answer.
Keep each answer under 8 lines.

Then start an interactive chat with the file passed to --prompt-file.

docker agent run embeddings.yaml --prompt-file pandas-users.md

Now every answer follows the instructions in pandas-users.md, even though the questions never mention pandas.

> How do I read a Parquet file without  it all into memory?

Polars: use scan_parquet("file.parquet") to get a LazyFrame (parsing is delayed).
Collect with streaming to process in batches: lf.collect(engine="streaming").
...
Pandas: read_parquet("file.parquet") loads the file into memory (no built-in
lazy/streaming scan).

If your audience knows SQL instead, create a prompt file called sql-users.md.

The reader knows SQL. Show the Polars SQL equivalent next to each Polars answer.
Keep each answer under 8 lines.

Start a chat with that file instead.

docker agent run embeddings.yaml --prompt-file sql-users.md

The same question now gets a SQL answer instead of a pandas one.

> How do I read a Parquet file without  it all into memory?

- Polars: Use lazy scanning so the file isn't parsed upfront:
  lf = pl.scan_parquet("data.parquet"), which returns a LazyFrame.
  SQL: SELECT * FROM read_parquet('data.parquet');
- Polars: Push down projections/filters so only needed columns/rows are read.
  SQL: SELECT col1, col2 FROM read_parquet('data.parquet') WHERE ...;
...

Add Variants with Flavors #

Sometimes you want more than one version of an agent, such as one with a different search method or a different model. It is redundant to write another YAML file for each version with most of the settings staying the same.

With docker-agent, you can add a flavor to the same file. A flavor lists only the settings that change, and everything else stays the same.

               --flavor hybrid
            off               on
             β”‚                 β”‚
             β–Ό                 β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ embedding search β”‚  β”‚ embedding search β”‚
   β”‚                  β”‚  β”‚ + keyword search β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       base config         base + hybrid

In this example, we add a flavor called hybrid that adds keyword search to the existing embedding search strategy.

agents:
  root: ...

rag:
  polars_docs:
    ...
    strategies:
      - type: chunked-embeddings  # embedding search

flavors:                          # new section at the bottom of the file
  hybrid:
    rag:
      polars_docs:
        strategies+:              # keep chunked-embeddings and add bm25
          - type: bm25            # keyword search
        results:
          fusion:
            strategy: rrf         # merge the keyword and embedding results
            k: 60
          deduplicate: true
          limit: 5

To use a flavor, pass its name with --flavor:

docker agent run embeddings.yaml

docker agent run embeddings.yaml --flavor hybrid

Here is what the config looks like after docker-agent applies the hybrid flavor.

rag:
  polars_docs:
    ...
    strategies:
      - type: chunked-embeddings      # from the base file
      - type: bm25                    # from hybrid
    results:
      fusion:
        strategy: rrf                 # from hybrid
      ...

To see how this is useful, ask about SQLContext, a class name that appears in the docs. Without the flavor, the agent uses the embedding search strategy and finds nothing.

docker agent run --exec embeddings.yaml "What is SQLContext?"
Calling polars_docs(query: "SQLContext")
Calling polars_docs(query: "SQL")

I searched the Polars documentation for "SQLContext" (and for "SQL") but found
no matching documents, so I cannot provide a definition from the docs.

With the hybrid flavor, every search runs both strategies. Keyword search finds the exact name, so the right page comes back.

docker agent run --exec embeddings.yaml --flavor hybrid "What is SQLContext?"
Calling polars_docs(query: "SQLContext")

SQLContext is the Polars object that manages SQL queries...

Sources:
- sql/intro.md

Run Different Agents from One File #

Sometimes you might want to have multiple agents for different purposes. For example, you might want to have a small model for answering questions and a stronger model for reviewing code.

docker-agent lets you add more agents to the same file. They can share tools already defined there, so you don’t need to repeat the configuration.

       root                               reviewer
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ gpt-5-mini        β”‚              β”‚ gpt-5             β”‚
 β”‚ answers questions β”‚              β”‚ reviews code      β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚                                  β”‚
           └──────────────┐   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β–Ό   β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  polars_docs    β”‚
                  β”‚  defined once   β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

In the code below, we add a second agent called reviewer that uses a stronger model for reviewing code but uses the same polars_docs tool as the root agent.

agents:
  root:
    model: openai/gpt-5-mini        # small model for answering questions
    instruction: |
      You answer questions about the Polars DataFrame library.
      ...
    toolsets:
      - type: rag
        ref: polars_docs          # shared search

  reviewer:
    model: openai/gpt-5             # stronger model for the harder task
    instruction: |
      You review Polars code.
      Search the docs for each function the code uses.
      Point out slower or outdated patterns and suggest the fix the docs recommend.
      ...
    toolsets:
      - type: rag
        ref: polars_docs          # same shared search

By default, docker-agent runs the first agent in the file. To run the reviewer instead, pass --agent.

docker agent run --exec embeddings.yaml --agent reviewer \
  'Review this code: df = pl.read_parquet("big.parquet").filter(pl.col("amount") > 0).select(["id", "amount"])'
Findings:
- Using read_parquet eagerly loads the entire file before your filter/select are
  applied. This prevents predicate and projection pushdown, which is slower and
  uses more memory on large Parquet files.

Recommended fix:
- Switch to scan_parquet and collect at the end:
  df = (
      pl.scan_parquet("big.parquet")
        .filter(pl.col("amount") > 0)
        .select(["id", "amount"])
        .collect()
  )
...

The reviewer spots the eager read_parquet call and suggests switching to scan_parquet.

Test the Agent #

As you experiment with different models and settings, you might want to make sure the agent keeps working as expected. It can be easy to miss mistakes by just observing the output.

docker-agents provides an easy way to define the test cases and run them against the agent.

 evals/streaming-parquet.json
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ question: How do I read a Parquet file without   β”‚
 β”‚            it all into memory?            β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β–Ό
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚    agent    β”‚
                   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                          β–Ό
                       answer
                          β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                  β–Ό                  β–Ό
 βœ“ uses               βœ“ mentions         βœ“ searched
   scan_parquet         streaming          the docs

To define a test case, create a JSON file in the evals/ folder, such as evals/streaming-parquet.json.

{
  "title": "Large Parquet file question",
  "messages": [
    {"message": {"message": {"role": "user", "content": "How do I read a Parquet file without  it all into memory?"}}}
  ],
  "evals": {
    "assertions": [
      { "name": "uses scan_parquet", "type": "contains", "value": "scan_parquet" },
      { "name": "mentions streaming", "type": "contains", "value": "streaming" },
      { "name": "searched the docs", "type": "tool_called", "value": "polars_docs" }
    ]
  }
}

For this question to pass, the answer must mention scan_parquet and streaming, and the agent must have searched the docs.

Now run the suite with docker agent eval:

docker agent eval embeddings.yaml ./evals
βœ“ Asof join question ($0.001918)
  βœ“ assertions 3/3
βœ“ Large Parquet file question ($0.002629)
  βœ“ assertions 3/3
[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% (2/2) βœ“2 βœ—0

βœ…     Assertions: 6/6 passed (100.0%)

Total Cost: $0.004546
Total Time: 26s

Each run reports which tests passed, what each answer cost, and how long it took. Together, they tell you whether a change broke the agent, made it pricier, or slowed it down.

Since each eval runs in its own container, one test can’t leave files behind that change another’s result.

Compare variants before you switch

Because eval takes --flavor too, you can test a variant before adopting it. Let’s say you want to know whether the cheaper gpt-5-nano is good enough for your use case. Add it as a second flavor called cheap.

flavors:
  hybrid: ...
  cheap:
    agents:
      root:
        model: openai/gpt-5-nano  # smallest, lowest-cost GPT-5 model

Then compare the default model against the cheap flavor. Since answers vary between runs, --repeat 3 runs each test three times.

docker agent eval embeddings.yaml ./evals --repeat 3
docker agent eval embeddings.yaml ./evals --flavor cheap --repeat 3
gpt-5-mini   gpt-5-nano (cheap)
assertions passed     18/18        13/18      <- less reliable
Parquet eval          3/3 runs     0/3 runs
asof join eval        3/3 runs     2/3 runs
total cost            $0.015       $0.007     <- less than half the cost

The cheap model saves more than half the cost, but its answers are less reliable. It passed 13 of 18 checks, compared with 18 of 18 for the default model.

Share the Agent #

Once you are happy with a configuration, you might want to share it with your teammates.

Sharing a Python agent usually means your teammates have to download the code, install the dependencies, and run a setup script before they can use it.

docker-agent makes sharing agents as easy as sharing Docker images. Teammates run it with one command instead of setting up a Python project.

To push the YAML file to a registry such as Docker Hub (after docker login), run:

docker agent share push embeddings.yaml docker.io/<username>/polars-docs:v1

Teammates can then run the agent by name, without down the file first.

docker agent run docker.io/<username>/polars-docs:v1

To get a local copy of the YAML file instead, use docker agent share pull.

Note that only the YAML file is shared. Any files it points to, such as docs folders or prompt files, must exist in the folder where you run the agent.

Final Thoughts #

What I like about docker-agent is that it takes so little code to get a working agent. An LLM can help you write a Python agent, but you still have to maintain that code, and more code means more places for mistakes.

Keeping everything in one YAML file also makes agents easy to change and easy to read, even for people who do not write Python every day. And with Docker-style sharing, your teammates can try your agent with one command.

But docker-agent is not the right fit for everything. If you need to control each step the agent takes, with custom branches or retries, a code-first framework like LangGraph is a better fit.

The same principle of keeping configuration separate from code is a core chapter in Production-Ready Data Science.

References #

  • docker-agent RAG docs (Docker, 2026): retrieval strategies, the default 0.5 similarity threshold, hybrid search with RRF fusion, and SQLite index storage.

Stay Current with CodeCut

Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @docker 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/writing-ai-agents-in…] indexed:0 read:14min 2026-10-28 Β· β€”