RFD 013: Named Query Templates
- Status: Draft
- Category: Design
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-02-25
Summary
This RFD introduces named query templates: reusable, config-defined templates that combine a Jinja-style content string with interactive questions to collect template variables. Users invoke a template by name (jp q -% feature) or pick one from a fuzzy-searchable list (jp q -%). The rendered template becomes the query sent to the assistant. Templates support configurable submit behavior and interactive question prompts with type validation.
Motivation
Today, jp query --template (-%) treats the query string itself as a Jinja-style template and resolves variables from template.values in the config. This works for simple interpolation, but has several limitations:
- No reuse. The template content lives in the query argument or the editor. There is no way to save a template and invoke it by name.
- No interactivity. All variables must be pre-configured in
template.values. There is no way to prompt the user for missing values at runtime. - No structure. Variables are untyped, have no descriptions, no defaults, no constrained choices. The user must know what variables exist and what values are valid.
- No discoverability. Without named templates, there is no way to list available templates or search for one.
The result is that users who want structured, repeatable queries (feature requests, code reviews, bug reports, architecture analyses) resort to shell scripts, aliases, or copy-pasting from notes. Named templates bring this workflow into JP, where it can integrate with the config system and the editor.
Design
User Experience
Invoking a template by name
# Long form
jp query --template feature
# Short form (the `%` flag accepts an optional value)
jp q -% featureThis loads the template named feature from the configuration, prompts the user for each question defined in the template, renders the content with the collected answers, and sends the result as the query.
Picking a template interactively
jp q -%When -% is passed without a value, JP presents a fuzzy-searchable list of all loaded templates (showing each template's title). The user selects one, then proceeds through the Q&A flow as above.
Submit behavior
After the Q&A, the rendered template is handled according to the template's submit field:
ask(default): Show the rendered template and prompt with an inline select menu:[s]end / [e]dit / [c]ancel / ?.unattended: Send the rendered template as the query immediately.edit: Open$EDITORwith the rendered template before sending.
The --edit (-e) and --no-edit (-E) CLI flags override the template's submit setting — --edit forces edit mode, --no-edit forces unattended mode.
Configuration Schema
Templates live under a top-level templates key. This replaces the current template key. The struct uses the same pattern as conversation.tools: a defaults field (accessed via templates.*) for global settings, and a flattened IndexMap for named templates.
# Global defaults (accessed via templates.*)
[templates.'*']
values = { project = "jp" }
submit = "ask"
# Named template — "feature" is the template name
[templates.feature]
title = "Building A New Feature"
description = "Structured template for proposing a new feature."
submit = "edit"
content = """
I want to build a new feature for {{ project }}.
## Context
{{ context }}
## Requirements
{% for req in requirements %} {# future: list-type questions #}
- {{ req }}
{% endfor %}
"""
[[templates.feature.questions]]
target = "project"
question = "What project is this for?"
type = "string"
[[templates.feature.questions]]
target = "context"
question = "Describe the context and motivation."
type = "string"Full schema for the defaults (templates.*):
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
values | object | n | {} | Global template variable values. |
| Pre-fills answers for any template. | ||||
submit | string | n | ask | Default submit mode: ask, |
unattended, or edit. |
Full schema for a named template:
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
title | string | y | — | Human-readable name shown in the picker. |
description | string | n | — | Shown below the title in the picker. |
submit | string | n | (default) | What happens after rendering: ask, |
unattended, or edit. | ||||
content | string | y | — | Minijinja template body. |
questions | array | n | [] | Questions that populate template |
| variables. |
Full schema for a question:
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
target | string | y | — | Variable name in the template content. |
question | string | y | — | Prompt shown to the user. |
type | string | n | string | Expected type: string, number, |
bool, text. | ||||
enum | array | n | — | Finite list of allowed values. Turns the |
| prompt into a selection menu. | ||||
default | any | n | — | Default value if the user provides none. |
CLI Changes
The existing -% / --template flag changes from a boolean to an optional string:
# Before (current)
-% / --template Boolean flag. Treats query as a template.
# After
-% / --template [NAME] Optional value.
No value, no query: show template picker.
With value: load named template.
No value, with query: treat query input as
inline template.The inline template mode (current -% behavior) keeps its current behavior. The query input comes from the usual sources: positional args, -- args, or stdin. For example:
# Inline template from positional arg
jp q -% -m sonnet "Hello {{ user }}"
# Inline template from stdin
echo "Hello {{ user }}" | jp q -%
# Inline template via -- separator
jp q -% -- "Hello {{ user }}"
# Named template (no query arg needed)
jp q -% feature
# Picker (no query arg needed)
jp q -%Interactive Q&A Flow
When a named template is loaded:
- For each entry in
questions, in order:- If the variable already has a value in
templates.*.values, skip. - If
enumis set, show a selection prompt (usinginquire::Select). - Otherwise, show a text prompt with the
questionstring. Ifdefaultis set, pre-fill it. - Validate the answer against
type. On validation failure, re-prompt. - Esc skips the current question (leaves it blank, which will produce a template rendering error if the variable is used and minijinja is in strict mode).
- If the variable already has a value in
- Render the
contenttemplate with the collected values. - Handle the result according to
submitmode:ask: Show rendered template and prompt[s]end / [e]dit / [c]ancel.unattended: Send immediately.edit: Open$EDITOR.
Values from templates.*.values in the config serve as pre-filled answers. This lets users hardcode values they always use (e.g., templates.*.values.project = "jp") while still being prompted for the rest.
Internal Design
Config Changes (jp_config)
The existing TemplateConfig is replaced with TemplatesConfig, using the same defaults + flattened IndexMap pattern as ToolsConfig:
/// Templates configuration.
#[derive(Config)]
#[config(rename_all = "snake_case", allow_unknown_fields)]
pub struct TemplatesConfig {
/// Global defaults, accessed via `templates.*`.
#[setting(nested, rename = "*")]
pub defaults: TemplateDefaults,
/// Named templates (catch-all for unknown keys).
#[setting(nested, flatten, merge = merge_nested_indexmap)]
templates: IndexMap<String, NamedTemplate>,
}
/// Global template defaults.
pub struct TemplateDefaults {
/// Template variable values used to render query templates.
pub values: Map<String, Value>,
/// Default submit behavior for all templates.
#[setting(default = SubmitMode::Ask)]
pub submit: SubmitMode,
}
pub struct NamedTemplate {
pub title: String,
pub description: Option<String>,
pub submit: Option<SubmitMode>,
pub content: String,
pub questions: Vec<TemplateQuestion>,
}
pub struct TemplateQuestion {
pub target: String,
pub question: String,
#[serde(default = "default_type")]
pub r#type: QuestionType,
#[serde(default)]
pub r#enum: Option<Vec<String>>,
pub default: Option<Value>,
}
pub enum QuestionType {
String,
Number,
Bool,
}
pub enum SubmitMode {
Ask,
Unattended,
Edit,
}The top-level config key changes from template to templates. The AppConfig field updates accordingly. This is a breaking change to the existing template.values config path (now templates.*.values or just templates.values).
Query Command Changes (jp_cli)
The --template flag changes from bool to Option<Option<String>>:
/// Use a named template, or treat the query as an inline template.
///
/// Without a value: show interactive template picker.
/// With "-": treat query input as an inline minijinja template.
/// With any other value: load the named template.
#[arg(short = '%', long)]
template: Option<Option<String>>,The build_conversation method gains a branch:
template == Some(Some("-"))→ inline template mode (query input is a minijinja template, rendered withtemplates.*.values).template == Some(Some(name))→ load named template, run Q&A, render.template == Some(None)→ show picker, then as above.template == None→ no template processing.
The Q&A loop and template rendering can be extracted into a function in jp_cli (or a new jp_template crate if the logic grows).
Drawbacks
- Config namespace. Named templates share the
templatesnamespace with default fields (values,submit). Template names must not collide with these reserved names. The list of reserved names is small and stable, and schematic will produce a clear error on collision. - Complexity budget. The Q&A flow, validation, and submit modes add surface area to the query command, which is already the largest module in the codebase.
- Minijinja learning curve. Users need to learn minijinja syntax to write templates. This is mitigated by the simple variable interpolation being trivial (
). - Breaking change. The rename from
templatetotemplatesbreaks existingtemplate.valuesconfig paths. Migration is straightforward (rename the key) but must be documented.
Alternatives
Keep template (singular) as the top-level key
Avoids the breaking rename but creates a namespace where values coexists with named templates in a less principled way. The templates rename aligns with the pattern established by conversation.tools and makes the defaults/flatten split explicit.
Nested namespace: [templates.definitions.feature]
Avoids namespace collision entirely but reads poorly in TOML and adds a redundant nesting level. Rejected in favor of the flat namespace with reserved-name checking, following the ToolsConfig precedent.
External template files
Store templates as separate .md or .toml files in a templates/ directory. More flexible for large templates, but adds file discovery complexity and diverges from the config-centric approach. Could be added later as a complement (e.g. content_file = "templates/feature.md").
Prompt-driven template creation (no config)
Instead of config-defined templates, let users create templates interactively with jp template create. More discoverable, but harder to version control and share. The config-based approach is preferred because templates are part of the project's configuration and travel with the repository.
Non-Goals
- Template sharing/registry. No mechanism for publishing or installing templates from external sources.
- Template versioning. Templates are config values; they version with the config file in Git.
- Complex control flow in Q&A. No conditional questions, branching, or loops in the question sequence. Each question is independent.
- Custom template functions. Noted as a future extension point but not designed here.
- Non-query templates. This is specifically for
jp query. Other commands may benefit from templates in the future, but that's out of scope. - Template-level config overrides. A template does not carry its own model, tools, or other config settings. Instead, templates can live in separate config files loaded via
jp -c my_template -%tmpl, which lets the config file set any options alongside the template definition. This uses the existing config layering system rather than adding a template-specific override mechanism.
Risks and Open Questions
Should template names be validated at config load time?
Yes. If a template references variables in content that have no corresponding question and no value in templates.*.values, this should produce a warning at config load time (not an error, since a user may rely on templates.*.values to provide the variable at runtime).
How does --template interact with --schema?
They are independent. --schema constrains the assistant's response format. --template constructs the query content. Both can be used together: a template generates the query, and a schema constrains the response.
How does --template interact with --replay?
--replay re-sends the last message. --template constructs a new message. They conflict. The CLI should enforce this with conflicts_with.
Implementation Plan
Phase 1: Config schema and named template loading
Replace TemplateConfig with TemplatesConfig using the defaults + flattened IndexMap pattern. Add NamedTemplate, TemplateQuestion, TemplateTarget, SubmitMode types. Add serialization tests. Add config validation (reserved name check, variable coverage warning). Update AppConfig to use the new type. This touches jp_config only and can be merged independently.
Phase 2: CLI flag change and template picker
Change --template from bool to Option<Option<String>>. Implement the fuzzy-searchable template picker using inquire::Select (showing title and description). Wire up the named template loading path in build_conversation, including the -%- inline mode. At this point, selecting a template loads it but does not yet run the Q&A — it renders with whatever values are available in templates.*.values.
Depends on Phase 1. Can be merged independently.
Phase 3: Interactive Q&A
Implement the question loop: text prompts, selection prompts for enum fields, default values, type validation, Esc (skip one), Ctrl+D (skip all). Integrate with the inquire crate (already a dependency via jp_inquire). Wire the collected answers into the minijinja rendering context alongside templates.*.values.
Depends on Phase 2. Can be merged independently.
Phase 4: Submit behavior
Implement the submit field: ask (inline select menu), unattended (send immediately), edit (open $EDITOR). Honor --edit / --no-edit overrides. The ask mode reuses the InlineSelect component from jp_inquire. The edit mode reuses edit_message.
Depends on Phase 3. Can be merged independently.
Future Work
- Assistant-populated variables. A
targetfield (user|assistant) on templates and/or individual questions could allow delegating unanswered questions to the assistant via structured output. Unanswered questions would be converted to a JSON schema, sent as a structured output request, and the response would fill in the remaining template variables. This depends on the structured output infrastructure and needs real use cases to justify the added complexity. - Custom template functions. Expose Rust functions in the minijinja environment (e.g.
get_config(),model_id(),git_branch(),env()). answermode. A field controlling the Q&A process itself (ask,unattended,edit) — e.g. skipping prompts when all defaults are present.
References
- Issue #178: Add named templates with interactive prompts for
jp query jp_config::template(crates/jp_config/src/template.rs) — current template configjp_config::conversation::tool(crates/jp_config/src/conversation/tool.rs) —ToolsConfigpattern (defaults + flatten)jp_cli::cmd::query(crates/jp_cli/src/cmd/query.rs) — current template rendering (line ~510)- GitHub issue template syntax — prior art for template question schemas
- minijinja documentation