Skip to main content

Getting Creative with Prompt Generation and Nushell Completions

Submitted by Lennart on

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 identifier
  • description: Appears in the completion menu for readability
  • prompt: 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:

  1. Getting the prompt text for each component
  2. Prepending section labels (ROLE:, TASK:, FORMAT:)
  3. Appending the original context data
  4. 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:

  1. Create a new .nuon file following the {value, description, prompt} structure
  2. Write a completion function that reads and returns the file
  3. Add the completion reference to your function signature
  4. 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.