cd /news/ai-agents/intro-to-goose-recipes-and-sub-recip… · home topics ai-agents article
[ARTICLE · art-111155] src=adrianavillela.netlify.app ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Intro to Goose Recipes and Sub-Recipes

Goose, an open-source AI agent framework originally created by Block and donated to the Agentic AI Foundation (AAIF), enables users to build reusable, parameterized agentic workflows called recipes, which support sub-recipes as helper functions. The article by avillela explains the anatomy of a Goose recipe using a YAML example that deploys a local Kubernetes cluster with KinD, detailing components such as version, title, description, instructions, prompt, parameters, extensions, settings, and author.

read13 min views6 publishedAug 25, 2026
Intro to Goose Recipes and Sub-Recipes
Image: Adrianavillela (auto-discovered)

Using Goose to create re-usable and shareable prompts #

Last year, as I was starting my agentic AI journey, a tool called Goose caught my eye. I’ve talked about Goose on a few occasions in the past year. 😄

Goose is an open source AI agent framework originally created by Block (the company formerly known as Square), which was recently donated to the Agentic AI Foundation (AAIF).

I like Goose because it provides an abstraction layer on top of your LLM (e.g. Claude Sonnet, GPT-5.6 Sol, Gemini Flash) and provider (e.g. Claude, GitHub Copilot, Gemini). This means that you can swap out provider and model combinations but keep your prompts the same. (Well, in theory, anyway…not all LLMs are created equal. 🫠) This came in handy for me last year when I was playing around with Goose and ran out of premium tokens on GitHub Copilot and had to switch my model from Claude Sonnet to GPT-4.o. But also, GPT-4.o is no Claude Sonnet.

Anyhooooo…there’s another reason why I like Goose: you can use it to create reusable, parameterized agentic workflows, which you can share within and outside of your organization. I like to think of Goose recipes as the Ansible Playbook of agentic workflows. It also supports sub-recipes. Think of these as helper functions.

And with that in mind, today I’ll be digging into how Goose recipes and sub-recipes work. Let’s get started!

Anatomy of a Goose recipe # #

Below is an example Goose recipe that I wrote called deploy-local-k8s-cluster.yaml

, which creates a KinD cluster.

version: 1.0.0
title: Local Kubernetes Deployment
description: This recipe deploys and configures a local Kubernetes cluster using KinD
instructions: |

  ## Persona

  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.

  ## Rules/inputs/constraints

  * Install `kubectl` if it's not already installed.
  * Use KinD to create a local Kubernetes cluster.
    - Install `kind` if it's not already installed.
  * Install `docker` if it's not already installed.

  ## Output format

  Summarize task output as follows:
  * Results in tabular format
  * The summary table should have 2 columns:
    - First column: Component
    - Second column: Details

prompt: |

  ## Objective

  Create a new KinD cluster.

  Tasks:
  * Create a single-node local Kubernetes cluster named {{ cluster_name }}.
    - If the cluster {{ cluster_name }} already exists, delete the old one and create a new one
  * Cluster must be accessible via `kubectl` post-install
  * This task is not considered complete until the kubernetes cluster is up and running. This means:
    - Node status is "Ready"
    - All system pods are running
  * Provide output summary as per output instructions above. Do NOT deviate from the format.

parameters:
  - key: cluster_name
    input_type: string
    requirement: required
    default: "my-cluster"
    description: "Name of the k9s cluster to create"

extensions:
- type: builtin
  name: developer
  display_name: Developer Tools
  description: null
  timeout: 300
  bundled: true
  available_tools: []

settings:
  goose_provider: anthropic
  goose_model: claude-sonnet-5
  temperature: 0.0

author:
  contact: avillela

Let’s break it down:

version

: The version of the recipe (the can be whatever you want). - title

: Name of the recipe. - description

: What is your recipe about? - instructions

: General directives for the model to follow when executing the recipe. These include things like:persona: What role is the model taking on?** rules/inputs/constraints**: This includes things like dos and don’ts, and links to reference documentation.** outputs**: What do you want the model to output in the end, and in what format?

prompt

: The actual things you want the recipe to do. I like to provide a task list. Don’t try to do too many things in the prompt. You should have one specific goal, and the tasks should be related to that goal. - parameters

: Just like you can pass parameters to a script or function, you can pass parameters to a Goose recipe! You must define the parameter name, default value, parameter type, and whether it’s mandatory or optional. You reference parameters in the recipe using double curly braces:{{ parameter_name }}

settings

: This section is totally optional. If you include it, it overrides the provider settings in~/.config/goose/config.yaml

. The settings values include:goose_provider: In our case, it’s setanthropic

. To use this provider, you will need anANTHROPIC_API_KEY

. You are prompted for this value to set up theanthropic

provider when you rungoose configure

. You can also set it as an environment variable. Be sure to set it before runninggoose session

orgoose run --recipe <recipe_name>

, though!goose_model: In our case, it’s set toclaude-sonnet-5

.temperature: In our case, it’s0.0

. Temperature is a value between 0 and 1, and it specifies how much creativity the model should have. The closer to 1, the more creative liberty the model has.max_turns: Maximum number of iterations Goose can spend on the prompt. This is handy to prevent Agent Infinite Loop of Doom (TM). The default value is 1000 for the parent recipe, and 25 for sub-recipes. You can set this value globally through theGOOSE_SUBAGENT_MAX_TURNS

variable in yourGoose config.yaml, but remember that settingmax_turns

inside the recipe will override the global value.

extensions

: Specify whichextensionsyou want to include in this recipe. This overrides the extensions configuration in yourGoose config.yaml. Extensions are Goose speak for MCP servers registered with Goose. Goose also comes with its ownbuilt-in extensions. For example, thedeveloper extensionenables Goose to do stuff like update files and execute commands in your file system. - author

: Author name.

If you want to get really fancy, you can check out some of the other additional recipe fields. But rest assured, the ones include eveything you need to get started.

I usually put my Goose recipes in a ./goose/recipes

folder:

./goose/
  └── recipes/
      └── deploy-local-k8s-cluster.yaml

You can also configure a default recipe location by setting the variable GOOSE_RECIPE_PATH

either as an environment variable or in your Goose config.yaml. For more on recipe locations, check out the Goose docs.

To run the above recipe from your repository root:

goose run --recipe goose/recipes/deploy-local-k8s-cluster.yaml

This creates a Kubernetes KinD cluster called my-cluster

, as per the default value of the cluster_name

paramters.

If you want to override the default value of the cluster_name

parameter, can pass in the parameter name/value pair at runtime:

goose run --recipe ./goose/recipes/deploy-local-k8s-cluster.yaml --params cluster_name=my-kind-cluster

Sub-recipes # #

But wait! There’s more!! You can also define sub-recipes. As I said earlier, you can think of sub-recipes as helper functions. Let’s take a look at an example by using our KinD cluster example as a base.

Suppose that in addition to creating the KinD cluster, we’re also installing the OTel Operator on the cluster. In this case, we should have a parent recipe that calls 2 sub-recipes: one to create the KinD cluster, and one to install the OTel Operator. Our folder structure therefore looks like this:

./goose/
  └── recipes/
      ├── bootstrap-k8s-cluster.yaml
      └── subrecipes/
          ├── create-kind-cluster.yaml
          └── install-otel-operator.yaml

And now our root recipe looks like this:

version: 1.0.0
title: Local Kubernetes Deployment
description: This recipe deploys and configures a local Kubernetes cluster and installs the OTel Operator
instructions: |

  ## Persona
  
  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.

  ## Rules/inputs/constraints

  * Install `kubectl` if it's not already installed.
  * Install `docker` if it's not already installed.
  * Respect the paramters being passed from the main recipe to the sub-recipes.

  ## Output format

  Summarize sub-recipe output as follows:
  * Results in tabular format
  * The summary table should have 2 columns:
    - First column: Component
    - Second column: Details

parameters:
  - key: cluster_name
    input_type: string
    requirement: required
    default: "my-cluster"
    description: "Name of the k9s cluster to create"

sub_recipes:
  - name: "create_kind_cluster"
    path: "./subrecipes/create-kind-cluster.yaml"
  - name: "install_otel_operator"
    path: "./subrecipes/install-otel-operator.yaml"

prompt: |

  ## Objective

  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:
    1. create_kind_cluster
    2. install_otel_operator

settings:
  goose_provider: anthropic
  goose_model: claude-sonnet-5
  temperature: 0.0

extensions:
- type: builtin
  name: developer
  display_name: Developer Tools
  description: null
  timeout: 300
  bundled: true
  available_tools: []

author:
  contact: avillela

Here, I moved the KinD cluster creation logic out of the parent recipe, and into a sub-recipe called create_kind_cluster

. For fun and to make this example a little more exciting, I created another sub-recipe called install_otel_operator

, which installs the OTel Operator in the newly-created KinD cluster.

In the parent recipe, you’ll notice a new section called sub_recipes

:

sub_recipes:
  - name: "create_kind_cluster"
    path: "./subrecipes/create-kind-cluster.yaml"
  - name: "install_otel_operator"
    path: "./subrecipes/install-otel-operator.yaml"

Which references the two aforementioned sub-recipes. We must also update our prompt to tell Goose to run the sub-recipes in the order specified.

FUN FACT:Recipes can run[sequentially]or[in parallel]. We’re running them sequentially in our example.

prompt: |

  ## Objective

  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:
    1. create_kind_cluster
    2. install_otel_operator

Now let’s look at the sub-recipes.

First up, we have the create_kind_cluster

sub-recipe, which looks more or less like our original deploy-local-k8s-cluster.yaml

recipe:

version: 1.0.0
title: Local Kubernetes Deployment
description: This creates a local Kubernetes cluster using KinD
instructions: |

  ## Persona

  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.

  ## Rules/inputs/constraints

  * Use KinD to create a local Kubernetes cluster.
  * Install `kind` if it's not already installed.

  ## Output format

  Follow output format defined in main recipe.

parameters:
  - key: cluster_name
    input_type: string
    requirement: required
    description: "Name of the k8s cluster to create"

prompt: |

  ## Objective

  Create a new KinD cluster.

  Before creating the cluster, tell me the value of {{ cluster_name }}. It was passed in through the command line when the recipe was invoked.

  Tasks:
  * Create a single-node local Kubernetes cluster named {{ cluster_name }}.
  * Cluster must be accessible via `kubectl` post-install
  * This task is not considered complete until the kubernetes cluster is up and running. This means:
    - Node status is "Ready"
    - All system pods are running
  * Provide output summary as per output instructions

settings:
  goose_provider: anthropic
  goose_model: claude-sonnet-5
  temperature: 0.0

extensions:
- type: builtin
  name: developer
  display_name: Developer Tools
  description: null
  timeout: 300
  bundled: true
  available_tools: []

author:
  contact: avillela

and then we have install_otel_operator

:

version: 1.0.0
title: Install OTel Operator
description: This installs the OTel Operator in a k8s cluster
instructions: |

  ## Persona

  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.

  ## Rules/inputs/constraints

  * Install the OTel Operator via Helm chart
    - Operator Helm chart location: https://github.com/open-telemetry/opentelemetry-helm-charts/tree/main/charts/opentelemetry-operator
    - Helm chart version: 0.122.0
  * Install cert-manager via Helm chart
    - cert-manager Helm chart location: https://charts.jetstack.io
    - Helm chart version: v1.21.1

  ## Output format

  Follow output format defined in main recipe.

parameters:
  - key: cluster_name
    input_type: string
    requirement: required
    description: "Target k8s cluster"

prompt: |

  ## Objective

  Install the OTel Operator and its pre-requiste, cert-manager, in the cluster {{ cluster_name }}

  Tasks:
  * Install cert-manager in the Kubernetes Cluster {{ cluster_name }}
    - This task is not considered complete until all pods in the `cert-manager` namespaces are running
  * Install the OTel Operator in the Kubernetes Cluster {{ cluster_name }}
    - This task is not considered complete until the pods in the `otel-operator-system` namespace are running
  * Provide output summary as per output instructions

settings:
  goose_provider: anthropic
  goose_model: claude-sonnet-5
  temperature: 0.0

extensions:
- type: builtin
  name: developer
  display_name: Developer Tools
  description: null
  timeout: 300
  bundled: true
  available_tools: []

author:
  contact: avillela

As you can see, there’s nothing special about these sub-recipes, other than the fact that they are called from the parent recipe.

NOTE:As a rule of thumb, sub-recipes should run standalone, meaining that you can (and should) test them without having to run them via the parent recipe.

Gotchas # #

While working on the examples for this blog post, I ran into a few issues that sent my head spinning for the better part of the day. I thought I’d share some of my gotchas so that you can avoid my pain and stress.

Gotcha #1: claude-acp

provider does not respect Goose #

When I first started working on the sub-recipes example, I was using the claude-acp

provider. Unfortunately, I quickly noticed that when I ran my recipes, many of my Goose-isms were being ignored. It was as if the recipe would load, and then Claude code would take over, ignoring all of the recipe configurations.

A quick conversation with my AI overlord strengthened my suspicions, so I decided to put my theory to the test by switching my provider from claude-acp

to anthropic

, and boom. 💥 Problems solved. Check out this issue to feel my pain.

Gotcha #2: Parameter passing #

As I mentioned earlier, you can pass parameteres into a Goose recipe at runtime, which overrides its default value (if any). These should flow through from parent to sub-recipes.

When I first started running the sub-recipe example, I noticed that Goose was ignoring the value of the runtime paramter and kept trying to use the default value. It was INFURIATING!! 🤬

I spent HOURS trying to figure out WTF was going on. I eventually tracked it down to two things:

FIRST… The claude-acp

provider was not honouring Goose recipe/sub-recipe constructs, so I switched to the anthropic

provider.

But that still didn’t fully fix the issue. I initially tried to pass the parameter directly to the sub-recipes, as you would with a function call in good ‘ole regular code, like this:

sub_recipes:
  - name: "create_kind_cluster"
    path: "./subrecipes/create-kind-cluster.yaml"
    values:
      cluster_name: "{{ cluster_name }}"
  - name: "install_otel_operator"
    path: "./subrecipes/install-otel-operator.yaml"
    values:
      cluster_name: "{{ cluster_name }}"

But that wasn’t working.

So out of pure desperation, I removed the values

from the sub_recipes

(it wasn’t doing anything anyway), and changed my parent prompt from this:

prompt: |

  ## Objective

  Bootstrap a local kubernetes cluster by executing the subrecipes in the following order:
    1. create_kind_cluster
    2. install_otel_operator

to this:

prompt: |

  ## Objective

  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:
    1. create_kind_cluster
    2. install_otel_operator

Do you see the difference? It’s quite small, actually. All I did was mention {{ cluster_name }}

in the prompt. FOR REALZ.

OOF. That took way too long.

I still don’t know why I couldn’t have just passed in the parameter values

in sub_recipes

. If you’re reading this and happen to know the mysteries of Goose parameter passing, please hit me up. I’m all ears. But for now, at least I found a solution!

Final thoughts # #

Goose recipes are a great way to create shareable, reusable workflows within and outside your organization. I did find some ergonomic challenges when setting up my examples, but alas, these are just part of normal growing pains of software, and shouldn’t stop you from creating your own.

And now, please enjoy a photo of this cute dachshund puppy, from a puppy yoga class that I attended recently. (Yes, that is actually a thing!!) Isn’t he adorable? 🥰

Until next time, peace, love, and code. 🖖💜👩💻

── more in #ai-agents 4 stories · sorted by recency
── more on @goose 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/intro-to-goose-recip…] indexed:0 read:13min 2026-08-25 ·