> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing Prompt Files

> Version-controlled prompts with YAML configuration and structured message or instruction bodies

Prompts in Output live in `.prompt` files inside a `prompts/` folder — version-controlled, reviewable, and deployed with your code. No more strings scattered across your codebase or prompts locked in external dashboards.

```yaml generate_summary@v1.prompt theme={null}
---
provider: anthropic
model: claude-sonnet-4-20250514
temperature: 0.3
maxOutputTokens: 2000
---

<system>
You are a sales research assistant. You help sales teams prepare for calls by summarizing company information.

Be factual and concise. Never make up information. If something is unclear from the provided data, say so.
</system>

<user>
Research this company and provide a brief summary for a sales call:

Company: {{ company_name }}
Website content: {{ website_content }}

Include: what they do, target market, recent news if available, and potential pain points we could address.
</user>
```

A prompt file has two parts: YAML frontmatter (configuration) and a body containing either role-tagged messages or plain instructions.

## File Naming

Prompt files use the pattern `name@version.prompt`:

* `generate_summary@v1.prompt`
* `judge_summary@v1.prompt`
* `classify_lead@v2.prompt`

The version lets you iterate on prompts while keeping old versions around. When you reference a prompt in code, use the name without the `.prompt` extension:

```typescript theme={null}
await generateText({
  prompt: 'generate_summary@v1',
  variables: { company_name: 'Acme Corp', website_content: '...' }
});
```

Output searches recursively from your workflow's directory to find the prompt file.

## Passing Prompt Objects Directly

`generateText`, `generateTextWithStreaming`, `streamText`, `generateImage`, and `new Agent()` also accept a loaded or custom `Prompt` object. This lets you load a prompt once or supply one from another source:

```typescript theme={null}
import { generateText, loadPrompt } from '@outputai/llm';

const prompt = loadPrompt('generate_summary@v1', {
  company_name: 'Acme Corp',
  website_content: '...'
});

await generateText({ prompt });
```

A custom object must match the normalized `Prompt` shape:

```typescript theme={null}
import { generateText } from '@outputai/llm';
import type { Prompt } from '@outputai/llm';

const prompt: Prompt = {
  name: 'custom-summary',
  fileDir: process.cwd(),
  variables: {},
  config: {
    provider: 'openai',
    model: 'gpt-4o',
    maxSteps: 10,
    skills: []
  },
  messages: [
    { role: 'user', content: 'Summarize the latest account activity.' }
  ],
  instructions: null
};

await generateText({ prompt });
```

Prompt objects are already resolved and rendered, so `promptDir` and call-level `variables` cannot be used with them. `fileDir` remains the base directory for relative skill paths. Use canonical provider names in custom objects. Deprecated aliases are still accepted and normalized with a warning.

<Warning>
  Only accept prompt objects from trusted sources. They can select providers and models and load local skill files relative to `fileDir`.
</Warning>

## File Organization

Place prompt files in a `prompts/` subfolder within your workflow directory:

```
src/workflows/
├── lead_enrichment/
│   ├── workflow.ts
│   ├── steps.ts
│   ├── evaluators.ts
│   ├── types.ts
│   └── prompts/
│       ├── generate_summary@v1.prompt
│       └── judge_summary@v1.prompt
├── classify_tickets/
│   ├── workflow.ts
│   ├── steps.ts
│   └── prompts/
│       └── classify@v1.prompt
└── shared/
    └── prompts/
        └── check_factuality@v1.prompt   # Shared across workflows
```

The recursive search means you can also keep prompts alongside your workflow code if you prefer a flatter structure. For prompts used by multiple workflows, create a shared `prompts/` folder at a higher level.

## Frontmatter

The YAML frontmatter configures the LLM call.

### Required Fields

| Field      | Description      | Example                                                                                                               |
| ---------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `provider` | LLM provider     | `anthropic`, `openai`, `azure`, `amazon-bedrock`, `google-vertex`, `perplexity` (legacy aliases: `bedrock`, `vertex`) |
| `model`    | Model identifier | `claude-sonnet-4-20250514`, `gpt-4o`                                                                                  |

### Optional Fields

| Field                                          | Description                                                                                                                                    | Example                                                |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `temperature`                                  | Sampling temperature; supported range varies by provider                                                                                       | `0` for judges, `0.3` for summaries, `0.7` for general |
| `maxOutputTokens`                              | Maximum output tokens                                                                                                                          | `2000`                                                 |
| `maxTokens`                                    | Deprecated compatibility key; prefer `maxOutputTokens`                                                                                         | `2000`                                                 |
| `topP`                                         | Nucleus sampling value; usually set instead of `temperature`                                                                                   | `0.9`                                                  |
| `topK`                                         | Restrict sampling to the top K token choices                                                                                                   | `40`                                                   |
| `presencePenalty`                              | Penalize tokens already present in the prompt or generated output                                                                              | `0.2`                                                  |
| `frequencyPenalty`                             | Penalize tokens according to how often they appear                                                                                             | `0.2`                                                  |
| `stopSequences`                                | Stop text generation when one of these sequences is produced                                                                                   | `["END"]`                                              |
| `seed`                                         | Random seed for deterministic text or image generation when supported                                                                          | `42`                                                   |
| `maxSteps`                                     | Tool-loop iterations when tools or skills are present (default 10). Always a positive integer after `loadPrompt`.                              | `5`                                                    |
| `skills`                                       | Skill file or directory paths, relative to this prompt. YAML may be a string or array; after `loadPrompt` always `string[]` (`[]` if omitted). | `./skills`, `../shared/tone.md`                        |
| `tools`                                        | Provider-specific tools                                                                                                                        | Vertex `googleSearch`, OpenAI `webSearch`              |
| `providerOptions`                              | Provider-specific config                                                                                                                       | See below                                              |
| `messageOptions`                               | Named per-message `providerOptions` sets                                                                                                       | See [Prompt Caching](/packages/llm#prompt-caching)     |
| `n`, `size`, `aspectRatio`, `maxImagesPerCall` | Image generation fields                                                                                                                        | See [Image Output](/packages/llm#image-output)         |

`loadPrompt` retains `maxTokens` on `config` for backward compatibility. When `maxOutputTokens` is absent, it also copies the `maxTokens` value to `maxOutputTokens`. When both keys are set, `maxOutputTokens` takes precedence. Text generation APIs use `maxOutputTokens`.

All fields use **camelCase**. Unknown top-level keys throw. A snake\_case alias of a known field fails with a suggestion (`max_output_tokens` -> use `maxOutputTokens`). Provider-specific keys such as `effort` and `reasoningEffort` belong under `providerOptions`, not at the top level.

```yaml theme={null}
---
provider: anthropic
model: claude-sonnet-4-20250514
temperature: 0.3
maxOutputTokens: 2000
---
```

### Configuration Structure

Prompt configurations have two layers:

**1. Top-level config** — Standard AI SDK options:

```yaml theme={null}
provider: anthropic
model: claude-sonnet-4-20250514
temperature: 0.7        # Standard option
maxOutputTokens: 4000   # Standard option
maxSteps: 5             # Tool-loop ceiling when tools or skills are present (default 10)
```

**2. providerOptions** — Provider-specific and special options:

```yaml theme={null}
providerOptions:
  thinking:             # Special: AI SDK extension (top-level)
    type: enabled
    budgetTokens: 5000
  anthropic:            # Provider-specific options (nested)
    effort: medium
```

**When to use providerOptions:**

* Provider-specific options that aren't standard across all providers
* Special AI SDK extensions like `thinking` or `order` (AI Gateway)
* Multi-provider configurations (Vertex with multiple model types)

**When NOT to use providerOptions:**

* Shared top-level fields: `temperature`, `maxOutputTokens`, `topP`, `topK`, `presencePenalty`, `frequencyPenalty`, `stopSequences`, `seed`, `maxSteps`, `skills`, `tools`, `messageOptions`, and the image fields (`n`, `size`, `aspectRatio`, `maxImagesPerCall`)
* These go at the top level alongside `provider` and `model`

### Provider Options

Use `providerOptions` for provider-specific configuration.

**Anthropic-specific options:**

```yaml theme={null}
---
provider: anthropic
model: claude-sonnet-4-20250514
providerOptions:
  anthropic:            # Namespace for Anthropic-specific options
    effort: medium      # Not available on other providers
---
```

**OpenAI-specific options:**

```yaml theme={null}
---
provider: openai
model: gpt-4o
providerOptions:
  openai:               # Namespace for OpenAI-specific options
    maxToolCalls: 1
    reasoning: true
---
```

**Google Vertex with Gemini (important namespace note):**

```yaml theme={null}
---
provider: google-vertex
model: gemini-2.0-flash
providerOptions:
  google:               # Use 'google' for Gemini, not 'google-vertex'!
    useSearchGrounding: true
---
```

**Extended thinking (special top-level key):**

```yaml theme={null}
---
provider: anthropic
model: claude-sonnet-4-20250514
providerOptions:
  thinking:             # Special: stays at top level (not under 'anthropic')
    type: enabled
    budgetTokens: 5000
  anthropic:            # Provider-specific options
    effort: medium
---
```

**Common Provider Options Reference:**

| Provider        | Option               | Namespace    | Description                                     |
| --------------- | -------------------- | ------------ | ----------------------------------------------- |
| Anthropic       | `effort`             | `anthropic:` | Reasoning effort: `low`, `medium`, `high`       |
| OpenAI          | `reasoningEffort`    | `openai:`    | Reasoning effort for o1 models                  |
| OpenAI          | `maxToolCalls`       | `openai:`    | Maximum tool calls per turn                     |
| Vertex (Gemini) | `useSearchGrounding` | `google:`    | Enable Gemini search grounding                  |
| Vertex (Claude) | `effort`             | `anthropic:` | Claude models on Vertex use anthropic namespace |
| AI SDK          | `thinking`           | Top-level    | Extended thinking (not under provider)          |
| AI Gateway      | `order`              | Top-level    | Provider routing order                          |

**Google Vertex Provider Namespace Guide:**

When using `provider: google-vertex` (or the legacy alias `vertex`), the `providerOptions` namespace depends on the model:

* **Gemini models** → Use `google:` namespace
* **Claude models** → Use `anthropic:` namespace
* **Vertex-specific options** → Use `vertex:` namespace

## Prompt Body Modes

After rendering the template, Output determines how to parse the body from its first meaningful token. Leading whitespace and HTML comments (`<!-- ... -->`) do not affect this decision:

* If the first meaningful token is plain text, the whole trimmed body becomes `instructions` and `messages` is empty. Tags later in the body stay part of the instruction text.
* If the first meaningful token is a tag, Output enters message mode and validates the body as role-tagged markup. `instructions` is `null`.

Use instruction mode for image prompts or when consuming `loadPrompt()` results directly. `generateText`, `generateTextWithStreaming`, `streamText`, and `Agent` require message mode.

```yaml theme={null}
---
provider: openai
model: gpt-image-1
---

Create a cinematic product photograph of {{ product }}.
```

Use message mode when the provider should receive a conversation:

```yaml theme={null}
---
provider: anthropic
model: claude-sonnet-4-20250514
---

<system>You write concise product descriptions.</system>
<user>Describe {{ product }}.</user>
```

In message mode:

* Top-level tags must be `<system>`, `<user>`, or `<assistant>`.
* Between top-level blocks, only whitespace and HTML comments are allowed.
* Top-level self-closing tags and closing tags without an opening block are invalid.
* Every top-level block needs a matching closing tag.
* Tags with a different name inside a message are preserved as message content. This includes semantic tags, HTML-like examples, and code such as `Array<string>`.
* A nested non-self-closing tag with the same name as its message is ambiguous and throws. Escape literal examples, including both brackets, such as `&lt;user&gt;example&lt;/user&gt;`.

These rules are validated when the prompt loads. Invalid roles, root text, malformed attributes, and unclosed blocks produce explicit errors instead of being silently ignored.

## Message Blocks

Message blocks use XML-style tags to define the conversation structure.

### `<system>`

The system message sets the persona and constraints. It defines *who* the LLM is and *how* it should behave. This stays constant across requests.

```yaml theme={null}
<system>
You are a sales research assistant. You summarize company information for sales teams.

Rules:
- Be factual and concise
- Never make up information
- Focus on business-relevant details
</system>
```

### `<user>`

The user message is the actual request — *what* you want right now. This typically contains your variables.

```yaml theme={null}
<user>
Write a company summary for {{ company_name }}.

Industry: {{ industry }}
Company size: {{ size }} employees
Website content: {{ website_content }}
</user>
```

### `<assistant>`

The assistant block is for conversation history or response prefilling. Use it when you want to prime the model's response format. `generateText`, `generateTextWithStreaming`, and `streamText` send it as part of the prompt thread. `Agent` drops authored `<assistant>` blocks; it only seeds `<user>` turns (system goes to `instructions`). For Agent few-shot or prefills, put examples in `<system>` or `<user>`, or use `generateText`.

```yaml theme={null}
<assistant>
Based on my analysis, here is the company summary:
</assistant>
```

Authored `<tool>` blocks are not supported. AI SDK tool results use structured message parts tied to a preceding tool call; AI SDK creates these during tool execution, and Agent callers may supply them through `messages` or `messageStore`.

### `options`

The only supported attribute on a role tag is `options`, a space-separated list of names from frontmatter `messageOptions`. At load, those sets are merged from left to right onto the message as `providerOptions`; later sets win when the same provider option is repeated.

Give `options` an explicit quoted or unquoted value, such as `options="cached fast"` or `options=cached`. Bare `options` throws, while `options=""` is treated as absent. Every referenced name must exist in `messageOptions`, and any other role-tag attribute throws. See [Prompt Caching](/packages/llm#prompt-caching).

## System vs User

A common mistake is putting everything in the user message. The split matters:

**System message** (constant):

* Who the LLM is ("You are a sales research assistant")
* Behavioral constraints ("Never make up information")
* Output format requirements ("Respond in JSON")

**User message** (varies per request):

* The specific request
* The data to process
* Dynamic instructions based on input

This separation makes prompts easier to maintain. When you need to change the task, you edit the user message. When you need to change behavior, you edit the system message.

## Using Prompts

Call prompts from your steps using the generate functions from `@outputai/llm`.

### With generateText

For single-shot LLM calls, use `generateText`:

```typescript steps.ts theme={null}
import { step } from '@outputai/core';
import { generateText } from '@outputai/llm';
import { GenerateSummaryInput, GenerateSummaryOutput } from './types.js';

export const generateSummary = step({
  name: 'generateSummary',
  description: 'Generate a company summary from research data',
  inputSchema: GenerateSummaryInput,
  outputSchema: GenerateSummaryOutput,
  fn: async (input) => {
    const { result } = await generateText({
      prompt: 'generate_summary@v1',
      variables: {
        company_name: input.name,
        website_content: input.websiteContent
      }
    });

    return result;
  }
});

// types.ts
// import { z } from '@outputai/core';
//
// export const GenerateSummaryInput = z.object({
//   name: z.string(),
//   websiteContent: z.string()
// });
//
// export const GenerateSummaryOutput = z.string();
```

`generateText` also supports [skills](/prompts/skills) for on-demand instruction loading. List skill paths in the prompt frontmatter. Set `maxSteps` in the same frontmatter when the default of 10 is wrong.

### With Agent

For multi-step tool loops and stateful conversations, use the `Agent` class:

```typescript steps.ts theme={null}
import { step } from '@outputai/core';
import { Agent } from '@outputai/llm';
import { ReviewContentInput, ReviewContentOutput } from './types.js';

export const reviewContent = step({
  name: 'reviewContent',
  description: 'Review technical content with structured feedback',
  inputSchema: ReviewContentInput,
  outputSchema: ReviewContentOutput,
  fn: async (input) => {
    const agent = new Agent({
      prompt: 'writing_assistant@v1',
      variables: {
        content_type: 'documentation',
        focus: 'clarity',
        content: input.content
      }
    });
    const { text } = await agent.generate();
    return text;
  }
});
```

Use `generateText` for single-shot LLM calls. Use `Agent` when you need multi-step tool execution, conversation history, or a reusable agent instance. See the [Agents section](/packages/llm#agents) for the full API.

The `variables` object maps to the `{{ variable }}` placeholders in your prompt. For dynamic content like conditionals and loops, see [Templating](/prompts/templating).
