feat(recipe): support structured parameters (object/array) in recipe templates#8934
feat(recipe): support structured parameters (object/array) in recipe templates#8934jordigilh wants to merge 13 commits intoaaif-goose:mainfrom
Conversation
…templates Add Object and Array variants to RecipeParameterInputType enum and introduce render_recipe_content_with_structured_params() that accepts HashMap<String, serde_json::Value>, enabling recipes to receive nested objects and arrays directly without flattening to strings. This is an additive, backward-compatible change. Existing string-based parameter passing remains unchanged. The new function allows callers to pass structured data (JSON objects, arrays) that MiniJinja can render natively using dot-access and iteration. Includes unit tests covering: object field access, conditionals, array iteration, nested objects, and mixed scalar/structured params. Ref: aaif-goose#8917 Made-with: Cursor
MiniJinja raises an undefined error when accessing a non-existent field on an object (unlike Jinja2's default). Use 'is defined' test in the unit test to match MiniJinja semantics. Made-with: Cursor
Made-with: Cursor
…ay params - Serde round-trip: JSON and YAML deserialization of input_type: object/array via Recipe::from_content, plus direct enum serialization/deserialization - Negative/edge-case: empty array iteration, deeply nested object (3 levels), dot-access on scalar type (expected failure) Made-with: Cursor
Add object and array to the input_type reference table, Input Types section, parameter examples, and constraints callout. Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: adb49849c6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| Object, | ||
| /// Array parameter passed as a JSON array. | ||
| /// Enables iteration in templates: `{% for item in param %}` | ||
| Array, |
There was a problem hiding this comment.
Wire object/array input types into recipe rendering
Adding Object and Array as valid input_type values here lets these recipes parse and validate, but the active build path still renders with HashMap<String, String> (apply_values_to_parameters and render_recipe_content_with_params in crates/goose/src/recipe/build_recipe/mod.rs), so structured values are never materialized as JSON objects/arrays. In practice, templates using {{ signal.namespace }} or {% for item in findings %} will fail at render time even though the recipe schema now accepts those input types.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for calling this out. The latest push (2de4daa) addresses this: render_recipe_template now detects input_type: object/array and routes through render_recipe_content_with_structured_params, parsing the JSON string values into serde_json::Value before MiniJinja rendering. All existing callers benefit without API changes. Four integration tests through build_recipe_from_template cover this end-to-end (object, array, mixed, and invalid-JSON error cases).
| - key: findings | ||
| input_type: array | ||
| requirement: optional | ||
| description: "List of diagnostic findings" |
There was a problem hiding this comment.
Add default for optional findings example parameter
The new sample declares findings as requirement: optional but does not provide a default, which conflicts with the validator rule that optional parameters must include defaults (validate_optional_parameters in crates/goose/src/recipe/validate_recipe.rs). Users copying this reference snippet will hit a validation error immediately, so the docs now demonstrate a non-runnable recipe.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. Fixed in 2de4daa -- added default: "[]" to the findings example so it passes validate_optional_parameters.
- render_recipe_template now detects object/array input_type params and routes through render_recipe_content_with_structured_params, parsing JSON string values into serde_json::Value before template rendering - All existing callers (CLI, server, summon, execute_commands) benefit automatically since the change is internal to the build pipeline - Add to_structured_params helper that converts the string map to a Value map, parsing only keys whose input_type is object or array - Add 4 integration tests through build_recipe_from_template: object param, array param, mixed string+object, invalid JSON error - Fix docs example: add missing default for optional array parameter Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2de4daa8c1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let value = if structured_keys.contains(k.as_str()) { | ||
| serde_json::from_str(v).map_err(|e| { | ||
| anyhow::anyhow!( | ||
| "Parameter '{}' has input_type object/array but value is not valid JSON: {}", | ||
| k, | ||
| e | ||
| ) | ||
| })? |
There was a problem hiding this comment.
Validate structured parameter shape by input_type
to_structured_params currently treats object and array parameters identically and only checks that the value is valid JSON. That allows mismatched payloads (e.g., input_type: array receiving {...} or input_type: object receiving [...]) to pass parsing and fail later during template rendering with confusing MiniJinja errors (item.name/field access on the wrong type). Since input_type semantics are now exposed in schema/docs, this should reject mismatches at conversion time by checking Value::is_object() vs Value::is_array() per parameter key and returning a targeted validation error.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good call. Fixed in 83db531 -- to_structured_params now validates that input_type: object receives a JSON object and input_type: array receives a JSON array, returning a targeted error like Parameter 'signal' has input_type object but received an array instead of letting mismatches surface as confusing MiniJinja template errors. Two new tests cover both mismatch directions.
Reject object/array type mismatches at conversion time instead of letting them surface as confusing MiniJinja errors during rendering. to_structured_params now checks Value::is_object() for input_type: object and Value::is_array() for input_type: array, returning a targeted error message on mismatch. Made-with: Cursor
parse_recipe_content renders the template with no parameters to discover
variable names. With Lenient mode, dot-access on undefined values
(e.g., {{ signal.name }}) fails because Lenient forbids attribute access
on undefined. Chainable mode allows chaining, returning undefined instead
of erroring. This is needed for structured parameters (object/array)
that use dot-notation and iteration in templates.
The actual rendering phase still uses Strict mode via
render_recipe_content_with_params / render_recipe_content_with_structured_params.
Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f174a245aa
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| &preprocessed_content, | ||
| recipe_dir, | ||
| UndefinedBehavior::Lenient, | ||
| UndefinedBehavior::Chainable, |
There was a problem hiding this comment.
Keep scalar dot-access failures in template validation
Using UndefinedBehavior::Chainable during parse_recipe_content allows {{ name.field }} to pass validation even when name is a scalar parameter, because undefined attribute access no longer errors in the parse phase. Those recipes then fail later at render time under Strict behavior (render_recipe_content_with_params / structured renderer), so malformed templates are now accepted and only break at execution.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is already handled by the validation layer. validate_parameters_in_template only normalizes dot-access for parameters with input_type: object or array. For a scalar parameter name, the template variable name.field is NOT normalized, so the validation correctly reports "missing: name.field, unnecessary: name", rejecting the recipe before it reaches the renderer.
| let parsed: Value = serde_json::from_str(v).map_err(|e| { | ||
| anyhow::anyhow!( | ||
| "Parameter '{}' has input_type {} but value is not valid JSON: {}", | ||
| k, | ||
| input_type, | ||
| e | ||
| ) |
There was a problem hiding this comment.
Validate structured defaults before render-time JSON parsing
Object/array values are first parsed in to_structured_params, which runs only in the rendering path. As a result, an optional structured parameter with an invalid default (for example, non-JSON text) passes recipe validation and only fails when the default is injected and parsed during build, turning a schema error into a late runtime error.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. Fixed in 3e95363 -- validate_optional_parameters now checks that object/array parameter defaults are valid JSON matching their input_type (object must be {}, array must be []). Invalid defaults are rejected during recipe validation instead of surfacing as late runtime errors.
validate_parameters_in_template now recognizes that template variables like signal.name and signal.namespace are attribute accesses on the signal parameter when it has input_type object or array. Dot-suffixed template variables are normalized to their root key before comparison with parameter definitions. Made-with: Cursor
Add object and array to RecipeParameterInputType enum in the generated openapi.json and types.gen.ts to match the Rust source. Made-with: Cursor
Align recipeFormSchema.ts input_type enum with the generated RecipeParameterInputType to fix TypeScript type compatibility. Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7626d92fa9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let root = var.split('.').next().unwrap_or(var); | ||
| if structured_keys.contains(root) { | ||
| root.to_string() |
There was a problem hiding this comment.
Normalize indexed structured vars before parameter diff
The new normalization only strips dot-notation (var.split('.')), so structured accesses that use indexing (for example {{ findings[0].name }} or {{ signal["namespace"] }}) are left as full paths and compared directly against parameter keys. That makes validate_parameters_in_template report these as missing definitions even when the root parameter (findings/signal) is correctly declared, blocking valid structured recipes during validation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. Fixed in 3e95363 -- extract_root_identifier now handles both dot-notation (signal.name) and bracket notation (findings[0].name, signal["namespace"]) by finding the earliest . or [ delimiter to extract the root identifier.
- extract_root_identifier handles both dot-notation (signal.name) and bracket notation (findings[0].name, signal["ns"]) when normalizing template variables to parameter keys - validate_optional_parameters now checks that object/array parameter defaults are valid JSON matching their input_type, catching invalid defaults at validation time instead of at render time Made-with: Cursor
…g_slice `extract_root_identifier` previously used `&var[..pos]` which clippy flags as potentially panicking on multi-byte UTF-8 characters. Replace with `str::split` on `.` and `[` delimiters which is both safe and more concise. Made-with: Cursor
Summary
Add
objectandarrayvariants toRecipeParameterInputTypeand wirestructured parameter support end-to-end through the build pipeline, enabling:
{{ signal.namespace }}{% for item in findings %}{{ item.name }}{% endfor %}{% if signal.severity == "critical" %}Callers pass JSON strings via the existing
Vec<(String, String)>API; thebuild pipeline detects
input_type: object/array, parses the strings intoserde_json::Value, and routes through the structured MiniJinja renderer.No API signature changes to
build_recipe_from_templateor its callers.Related discussion: #8917
Motivation
The current recipe template rendering accepts only
HashMap<String, String>,requiring callers to flatten structured data into individual string keys.
For recipes that process complex inputs (multi-field signals, arrays of
findings, enrichment metadata), this creates an impedance mismatch --
MiniJinja natively supports dot-notation, iteration, and conditionals on
structured data.
Changes
How it works
All callers (`goose-cli`, `goose-server`, `summon`, `execute_commands`) benefit automatically since the change is internal to the build pipeline.
Scope and limitations
Security model
The new function uses the same MiniJinja `Environment` setup as the existing
`render_recipe_content_with_params`:
of parameter structure
Test plan