{"slug": "intro-to-goose-recipes-and-sub-recipes", "title": "Intro to Goose Recipes and Sub-Recipes", "summary": "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.", "body_md": "# Intro to Goose Recipes and Sub-Recipes\n\n## Using Goose to create re-usable and shareable prompts\n\nLast year, as I was starting my agentic AI journey, a tool called [Goose](https://goose-docs.ai) caught my eye. [I’ve talked about Goose on a few occasions](/tag/goose/) in the past year. 😄\n\nGoose is an open source AI agent framework [originally created by Block](https://block.xyz/inside/block-open-source-introduces-codename-goose) (the company formerly known as Square), which was recently donated to the [Agentic AI Foundation (AAIF)](https://aaif.io/).\n\nI 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](post/prompt-based-reusable-workflows-for-lazy-developers/#lesson-2-llms-are-on-a-spectrum). 🫠) 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.\n\nAnyhooooo…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](https://www.redhat.com/en/topics/automation/what-is-an-ansible-playbook) of agentic workflows. It also supports sub-recipes. Think of these as helper functions.\n\nAnd with that in mind, today I’ll be digging into how Goose recipes and sub-recipes work. Let’s get started!\n\n## Anatomy of a Goose recipe[ #](#anatomy-of-a-goose-recipe)\n\nBelow is an example Goose recipe that I wrote called `deploy-local-k8s-cluster.yaml`\n\n, which creates a KinD cluster.\n\n```\nversion: 1.0.0\ntitle: Local Kubernetes Deployment\ndescription: This recipe deploys and configures a local Kubernetes cluster using KinD\ninstructions: |\n\n  ## Persona\n\n  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.\n\n  ## Rules/inputs/constraints\n\n  * Install `kubectl` if it's not already installed.\n  * Use KinD to create a local Kubernetes cluster.\n    - Install `kind` if it's not already installed.\n  * Install `docker` if it's not already installed.\n\n  ## Output format\n\n  Summarize task output as follows:\n  * Results in tabular format\n  * The summary table should have 2 columns:\n    - First column: Component\n    - Second column: Details\n\nprompt: |\n\n  ## Objective\n\n  Create a new KinD cluster.\n\n  Tasks:\n  * Create a single-node local Kubernetes cluster named {{ cluster_name }}.\n    - If the cluster {{ cluster_name }} already exists, delete the old one and create a new one\n  * Cluster must be accessible via `kubectl` post-install\n  * This task is not considered complete until the kubernetes cluster is up and running. This means:\n    - Node status is \"Ready\"\n    - All system pods are running\n  * Provide output summary as per output instructions above. Do NOT deviate from the format.\n\nparameters:\n  - key: cluster_name\n    input_type: string\n    requirement: required\n    default: \"my-cluster\"\n    description: \"Name of the k9s cluster to create\"\n\nextensions:\n- type: builtin\n  name: developer\n  display_name: Developer Tools\n  description: null\n  timeout: 300\n  bundled: true\n  available_tools: []\n\nsettings:\n  goose_provider: anthropic\n  goose_model: claude-sonnet-5\n  temperature: 0.0\n\nauthor:\n  contact: avillela\n```\n\nLet’s break it down:\n\n-\n`version`\n\n: The version of the recipe (the can be whatever you want). -\n`title`\n\n: Name of the recipe. -\n`description`\n\n: What is your recipe about? -\n`instructions`\n\n: 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?\n\n-\n`prompt`\n\n: 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. -\n`parameters`\n\n: 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 }}`\n\n-\n`settings`\n\n: This section is totally optional. If you include it, it overrides the provider settings in`~/.config/goose/config.yaml`\n\n. The settings values include:**goose_provider**: In our case, it’s set`anthropic`\n\n. To use this provider, you will need an`ANTHROPIC_API_KEY`\n\n. You are prompted for this value to set up the`anthropic`\n\nprovider when you run`goose configure`\n\n. You can also set it as an environment variable. Be sure to set it before running`goose session`\n\nor`goose run --recipe <recipe_name>`\n\n, though!**goose_model**: In our case, it’s set to`claude-sonnet-5`\n\n.**temperature**: In our case, it’s`0.0`\n\n. 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 the`GOOSE_SUBAGENT_MAX_TURNS`\n\nvariable in your[Goose config.yaml](~/.config/goose/config.yaml), but remember that setting`max_turns`\n\ninside the recipe will override the global value.\n\n-\n`extensions`\n\n: Specify which[extensions](https://goose-docs.ai/docs/getting-started/using-extensions/)you want to include in this recipe. This overrides the extensions configuration in your[Goose config.yaml](~/.config/goose/config.yaml). Extensions are Goose speak for MCP servers registered with Goose. Goose also comes with its own[built-in extensions](https://goose-docs.ai/docs/getting-started/using-extensions/#built-in-extensions). For example, the[developer extension](https://goose-docs.ai/docs/mcp/developer-mcp)enables Goose to do stuff like update files and execute commands in your file system. -\n`author`\n\n: Author name.\n\nIf you want to get really fancy, you can check out some of the other additional [recipe fields](https://goose-docs.ai/docs/guides/recipes/recipe-reference/#extensions). But rest assured, the ones include eveything you need to get started.\n\nI usually put my Goose recipes in a `./goose/recipes`\n\nfolder:\n\n```\n./goose/\n  └── recipes/\n      └── deploy-local-k8s-cluster.yaml\n```\n\nYou can also configure a default recipe location by setting the variable `GOOSE_RECIPE_PATH`\n\neither as an environment variable or in your [Goose config.yaml](~/.config/goose/config.yaml). For more on recipe locations, check out the [Goose docs](https://goose-docs.ai/docs/guides/recipes/recipe-reference/#recipe-location).\n\nTo run the above recipe from your repository root:\n\n```\ngoose run --recipe goose/recipes/deploy-local-k8s-cluster.yaml\n```\n\nThis creates a Kubernetes KinD cluster called `my-cluster`\n\n, as per the default value of the `cluster_name`\n\nparamters.\n\nIf you want to override the default value of the `cluster_name`\n\nparameter, can pass in the parameter name/value pair at runtime:\n\n```\ngoose run --recipe ./goose/recipes/deploy-local-k8s-cluster.yaml --params cluster_name=my-kind-cluster\n```\n\n## Sub-recipes[ #](#sub-recipes)\n\nBut 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.\n\nSuppose that in addition to creating the KinD cluster, we’re also installing the [OTel Operator](/tag/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:\n\n```\n./goose/\n  └── recipes/\n      ├── bootstrap-k8s-cluster.yaml\n      └── subrecipes/\n          ├── create-kind-cluster.yaml\n          └── install-otel-operator.yaml\n```\n\nAnd now our root recipe looks like this:\n\n```\nversion: 1.0.0\ntitle: Local Kubernetes Deployment\ndescription: This recipe deploys and configures a local Kubernetes cluster and installs the OTel Operator\ninstructions: |\n\n  ## Persona\n  \n  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.\n\n  ## Rules/inputs/constraints\n\n  * Install `kubectl` if it's not already installed.\n  * Install `docker` if it's not already installed.\n  * Respect the paramters being passed from the main recipe to the sub-recipes.\n\n  ## Output format\n\n  Summarize sub-recipe output as follows:\n  * Results in tabular format\n  * The summary table should have 2 columns:\n    - First column: Component\n    - Second column: Details\n\nparameters:\n  - key: cluster_name\n    input_type: string\n    requirement: required\n    default: \"my-cluster\"\n    description: \"Name of the k9s cluster to create\"\n\nsub_recipes:\n  - name: \"create_kind_cluster\"\n    path: \"./subrecipes/create-kind-cluster.yaml\"\n  - name: \"install_otel_operator\"\n    path: \"./subrecipes/install-otel-operator.yaml\"\n\nprompt: |\n\n  ## Objective\n\n  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:\n    1. create_kind_cluster\n    2. install_otel_operator\n\nsettings:\n  goose_provider: anthropic\n  goose_model: claude-sonnet-5\n  temperature: 0.0\n\nextensions:\n- type: builtin\n  name: developer\n  display_name: Developer Tools\n  description: null\n  timeout: 300\n  bundled: true\n  available_tools: []\n\nauthor:\n  contact: avillela\n```\n\nHere, I moved the KinD cluster creation logic out of the parent recipe, and into a sub-recipe called `create_kind_cluster`\n\n. For fun and to make this example a little more exciting, I created another sub-recipe called `install_otel_operator`\n\n, which installs the [OTel Operator](/tag/otel-operator) in the newly-created KinD cluster.\n\nIn the parent recipe, you’ll notice a new section called `sub_recipes`\n\n:\n\n```\nsub_recipes:\n  - name: \"create_kind_cluster\"\n    path: \"./subrecipes/create-kind-cluster.yaml\"\n  - name: \"install_otel_operator\"\n    path: \"./subrecipes/install-otel-operator.yaml\"\n```\n\nWhich references the two aforementioned sub-recipes. We must also update our prompt to tell Goose to run the sub-recipes in the order specified.\n\n✨\n\nFUN FACT:Recipes can run[sequentially]or[in parallel]. We’re running them sequentially in our example.\n\n```\nprompt: |\n\n  ## Objective\n\n  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:\n    1. create_kind_cluster\n    2. install_otel_operator\n```\n\nNow let’s look at the sub-recipes.\n\nFirst up, we have the `create_kind_cluster`\n\nsub-recipe, which looks more or less like our original `deploy-local-k8s-cluster.yaml`\n\nrecipe:\n\n```\nversion: 1.0.0\ntitle: Local Kubernetes Deployment\ndescription: This creates a local Kubernetes cluster using KinD\ninstructions: |\n\n  ## Persona\n\n  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.\n\n  ## Rules/inputs/constraints\n\n  * Use KinD to create a local Kubernetes cluster.\n  * Install `kind` if it's not already installed.\n\n  ## Output format\n\n  Follow output format defined in main recipe.\n\nparameters:\n  - key: cluster_name\n    input_type: string\n    requirement: required\n    description: \"Name of the k8s cluster to create\"\n\nprompt: |\n\n  ## Objective\n\n  Create a new KinD cluster.\n\n  Before creating the cluster, tell me the value of {{ cluster_name }}. It was passed in through the command line when the recipe was invoked.\n\n  Tasks:\n  * Create a single-node local Kubernetes cluster named {{ cluster_name }}.\n  * Cluster must be accessible via `kubectl` post-install\n  * This task is not considered complete until the kubernetes cluster is up and running. This means:\n    - Node status is \"Ready\"\n    - All system pods are running\n  * Provide output summary as per output instructions\n\nsettings:\n  goose_provider: anthropic\n  goose_model: claude-sonnet-5\n  temperature: 0.0\n\nextensions:\n- type: builtin\n  name: developer\n  display_name: Developer Tools\n  description: null\n  timeout: 300\n  bundled: true\n  available_tools: []\n\nauthor:\n  contact: avillela\n```\n\nand then we have `install_otel_operator`\n\n:\n\n```\nversion: 1.0.0\ntitle: Install OTel Operator\ndescription: This installs the OTel Operator in a k8s cluster\ninstructions: |\n\n  ## Persona\n\n  You are a Platform Engineer. Your role is to deploy and configure infrastructure to be used by developers.\n\n  ## Rules/inputs/constraints\n\n  * Install the OTel Operator via Helm chart\n    - Operator Helm chart location: https://github.com/open-telemetry/opentelemetry-helm-charts/tree/main/charts/opentelemetry-operator\n    - Helm chart version: 0.122.0\n  * Install cert-manager via Helm chart\n    - cert-manager Helm chart location: https://charts.jetstack.io\n    - Helm chart version: v1.21.1\n\n  ## Output format\n\n  Follow output format defined in main recipe.\n\nparameters:\n  - key: cluster_name\n    input_type: string\n    requirement: required\n    description: \"Target k8s cluster\"\n\nprompt: |\n\n  ## Objective\n\n  Install the OTel Operator and its pre-requiste, cert-manager, in the cluster {{ cluster_name }}\n\n  Tasks:\n  * Install cert-manager in the Kubernetes Cluster {{ cluster_name }}\n    - This task is not considered complete until all pods in the `cert-manager` namespaces are running\n  * Install the OTel Operator in the Kubernetes Cluster {{ cluster_name }}\n    - This task is not considered complete until the pods in the `otel-operator-system` namespace are running\n  * Provide output summary as per output instructions\n\nsettings:\n  goose_provider: anthropic\n  goose_model: claude-sonnet-5\n  temperature: 0.0\n\nextensions:\n- type: builtin\n  name: developer\n  display_name: Developer Tools\n  description: null\n  timeout: 300\n  bundled: true\n  available_tools: []\n\nauthor:\n  contact: avillela\n```\n\nAs you can see, there’s nothing special about these sub-recipes, other than the fact that they are called from the parent recipe.\n\n✨\n\nNOTE: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.\n\n## Gotchas[ #](#gotchas)\n\nWhile 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.\n\n### Gotcha #1: `claude-acp`\n\nprovider does not respect Goose[ #](#gotcha-1-claude-acp-provider-does-not-respect-goose)\n\nWhen I first started working on the sub-recipes example, I was using the `claude-acp`\n\nprovider. 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.\n\nA 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`\n\nto `anthropic`\n\n, and boom. 💥 Problems solved. Check out [this issue](https://github.com/aaif-goose/goose/issues/5559) to feel my pain.\n\n### Gotcha #2: Parameter passing[ #](#gotcha-2-parameter-passing)\n\nAs 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.\n\nWhen 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!! 🤬\n\nI spent HOURS trying to figure out WTF was going on. I eventually tracked it down to two things:\n\nFIRST… The `claude-acp`\n\nprovider was not honouring Goose recipe/sub-recipe constructs, so I switched to the `anthropic`\n\nprovider.\n\nBut 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:\n\n```\nsub_recipes:\n  - name: \"create_kind_cluster\"\n    path: \"./subrecipes/create-kind-cluster.yaml\"\n    values:\n      cluster_name: \"{{ cluster_name }}\"\n  - name: \"install_otel_operator\"\n    path: \"./subrecipes/install-otel-operator.yaml\"\n    values:\n      cluster_name: \"{{ cluster_name }}\"\n```\n\nBut that wasn’t working.\n\nSo out of pure desperation, I removed the `values`\n\nfrom the `sub_recipes`\n\n(it wasn’t doing anything anyway), and changed my parent prompt from this:\n\n```\nprompt: |\n\n  ## Objective\n\n  Bootstrap a local kubernetes cluster by executing the subrecipes in the following order:\n    1. create_kind_cluster\n    2. install_otel_operator\n```\n\nto this:\n\n```\nprompt: |\n\n  ## Objective\n\n  Bootstrap a local kubernetes cluster {{ cluster_name }} by executing the subrecipes in the following order:\n    1. create_kind_cluster\n    2. install_otel_operator\n```\n\nDo you see the difference? It’s quite small, actually. All I did was mention `{{ cluster_name }}`\n\nin the prompt. FOR REALZ.\n\nOOF. That took way too long.\n\nI still don’t know why I couldn’t have just passed in the parameter `values`\n\nin `sub_recipes`\n\n. 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!\n\n## Final thoughts[ #](#final-thoughts)\n\nGoose 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.\n\nAnd 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? 🥰\n\nUntil next time, peace, love, and code. 🖖💜👩💻", "url": "https://wpnews.pro/news/intro-to-goose-recipes-and-sub-recipes", "canonical_source": "https://adrianavillela.netlify.app/post/intro-to-goose-recipes/", "published_at": "2026-08-25 00:00:00+00:00", "updated_at": "2026-08-26 03:13:18.054859+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Goose", "Block", "Agentic AI Foundation (AAIF)", "Claude Sonnet", "GPT-5.6 Sol", "Gemini Flash", "GitHub Copilot", "KinD"], "alternates": {"html": "https://wpnews.pro/news/intro-to-goose-recipes-and-sub-recipes", "markdown": "https://wpnews.pro/news/intro-to-goose-recipes-and-sub-recipes.md", "text": "https://wpnews.pro/news/intro-to-goose-recipes-and-sub-recipes.txt", "jsonld": "https://wpnews.pro/news/intro-to-goose-recipes-and-sub-recipes.jsonld"}}