When working with LLMs from the command line, constructing well-structured prompts can be tedious and error-prone. I recently built a Nushell module that leverages custom completions to make prompt generation interactive and consistent.
Here's how it works and how you can build similar tools.
The Core Idea: Completable Prompt Building
Instead of memorizing prompt structures or copying from a notes file, this module lets you build prompts step-by-step with tab completion. The system supports both English and Danish, with three configurable components:
- Roles: Define who the AI should act as (e.g., Journalist, Attorney, Communicator)
- Tasks: Specify what the AI should do (e.g., Summarize, Analyze, Expand)
- Formats: Control the output style (e.g., Visual list, Longform document, LinkedIn post)
Usage looks like this:
"some longer form context input here..." | prompt en role task format
# Tab through options for language, role, task, and format
# Get a complete structured prompt ready for your LLM
Custom Completions: The Secret Sauce
The real power comes from Nushell's custom completion system. Here's the pattern:
export def da [
role: string@da_roles
task: string@da_tasks
format: string@da_formats
] { ... }
def da_roles [] {
open ~/.config/nushell/modules/prompt/da_roles.nuon
}
The @da_roles suffix tells Nushell to use the da_roles function for tab completion. When you type da and press Tab, you get a menu of available roles. The completion function simply returns the list from a data file, letting you manage options externally.
This is record based.
For the list based dali command, there's more complex filtering:
def da_prompts [args: string] {
open ~/.config/nushell/modules/prompt/da_prompts.nuon | where value not-in ( $args | split words )
}
This filters out already-selected options, preventing duplicates when using variadic arguments.
Data-Driven Architecture with .nuon Files
Instead of hardcoding prompts in the module, I use .nuon files for maintainability. Each file contains an array of records:
[
{
value: "Journalist"
description: "Investigative reporter persona"
prompt: "Adopt an investigative journalist persona. Be factual and distinguish confirmed facts from uncertainties."
}
{
value: "Attorney"
description: "Legal professional persona"
prompt: "Write as an attorney: precise and objective. Structure by facts, legal basis, and risks."
}
]
This three-field structure (value, description, prompt) serves multiple purposes:
value: The completion key and reference identifierdescription: Appears in the completion menu for readabilityprompt: The actual text that gets inserted into the final prompt
Separating data from logic means you can add new roles, tasks, or formats by editing a JSON-like file without touching the function code.
Pipeline-First Function Design
The implementation leverages Nushell's pipeline input pattern:
export def da [
role: string@da_roles
task: string@da_tasks
format: string@da_formats
] {
let context = $in
let roles = open ~/.config/nushell/modules/prompt/en_roles.nuon
let tasks = open ~/.config/nushell/modules/prompt/en_tasks.nuon
let formats = open ~/.config/nushell/modules/prompt/en_formats.nuon
$roles | where value == $role | get prompt | str prepend 'ROLE: '
| append ($tasks | where value == $task | get prompt | str prepend 'TASK: ')
| append ($formats | where value == $format | get prompt | str prepend 'FORMAT: ')
| append "CONTEXT:"
| append $context
| to text --no-newline
}
The $in variable captures pipeline input as context. The function then reads the three data files, filters for the selected values, and assembles the final prompt by:
- Getting the prompt text for each component
- Prepending section labels (ROLE:, TASK:, FORMAT:)
- Appending the original context data
- Converting everything to a single text string
This approach transforms raw data into production-ready prompts with consistent structure.
Technical Implementation Details
A few key patterns make this work:
File I/O for completions: Each completion function uses open to read the corresponding .nuon file. This keeps completions in sync with available data automatically.
Where clause filtering: where value == $role extracts the correct prompt from the data array. The value field serves as both the completion key and the lookup reference.
Pipeline assembly: Using | append chains builds the prompt step by step. Each addition maintains the pipeline flow, making the data transformation explicit.
Final output: to text --no-newline converts the structured list into a single string without adding extra newlines, which is crucial for LLM compatibility.
Extending the System
Adding new prompt components is straightforward:
- Create a new
.nuonfile following the{value, description, prompt}structure - Write a completion function that reads and returns the file
- Add the completion reference to your function signature
- Include the prompt assembly logic in the function body
For new languages, you'd create parallel .nuon files (like fr_roles.nuon, fr_tasks.nuon) and corresponding completion functions. The main function just needs to reference the language-specific files.
Why This Pattern Works
This approach exemplifies shell philosophy: do one thing well and compose. Each completion function has a single responsibility—returning a list of options. The main function orchestrates those options into a usable prompt. The data files serve as a configuration layer that bridges the two.
For Nushell developers working with LLMs, this pattern offers a template for building interactive, composable tools that make repeated tasks faster and more consistent. The custom completions provide a polished user experience, while the modular data structure ensures maintainability as your prompt library grows.
Whether you're building a prompt system, a configuration wizard, or any multi-step CLI tool, Nushell's completion system combined with pipeline functions gives you a powerful foundation to work from.