> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extra-ai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Prompt Rendering

> How prompt files work, how variables get filled, and when rendering happens.

## Prompts live in files, not in YAML

You declare a path to a Markdown file. The engine loads and renders it per request.

```yaml theme={null}
agents:
  orders_agent:
    prompts:
      system: "prompts/orders/system.md"
      user: "prompts/orders/user.md"

orchestrators:
  router:
    prompts:
      orchestrator: "prompts/router/orchestrator.md"
      system: "prompts/router/system.md"
```

| Field          | Applies to               | Required |
| -------------- | ------------------------ | -------- |
| `orchestrator` | Orchestrators only       | **Yes**  |
| `system`       | Orchestrators and agents | No       |
| `user`         | Orchestrators and agents | No       |

***

## Variables

Prompt files can contain `{{variable}}` placeholders:

```markdown theme={null}
You are a support agent for Acme Store.
Today is {{current_date}}.
You are helping {{customer_name}}.

Your account tier: {{account_tier}}.
```

Variables are filled by **resolvers** — Python methods that run before the agent executes. The LLM never sees the variable names, only the rendered values.

Declare which resolvers a node uses:

```yaml theme={null}
resolvers:
  current_date:
    scope: shared
  customer_name:
    scope: shared
  account_tier:
    scope: agent

agents:
  orders_agent:
    prompts:
      system: "prompts/orders/system.md"
    resolvers: [current_date, customer_name, account_tier]
```

The resolver implementation (scaffolded by `generate`):

```python theme={null}
class SharedResolver:
    def current_date(self, ctx) -> str:
        return datetime.now().strftime("%Y-%m-%d")

    def customer_name(self, ctx) -> str:
        return ctx.headers.get("X-Customer-Name", "Customer")

class Resolver(SharedResolver):
    def account_tier(self, ctx) -> str:
        return fetch_account_tier(ctx.headers.get("X-Customer-Id"))
```

***

## When rendering happens

**At startup** — prompt file paths are validated. If a file doesn't exist, the engine fails before serving requests.

**Per request** — resolvers run, variables are substituted, the final prompt string is produced. Rendered prompts are never cached globally — each request gets a fresh render with its own resolved values.

This means dynamic values like `current_date`, `customer_name`, or `account_tier` are always accurate per request, regardless of when the engine started.

***

## Rules

* A variable with no matching resolver fails clearly — the engine names the node, the variable, and what's missing.
* Prompt text is not a security boundary. Don't rely on prompt instructions to enforce access control — use the [access plugin](/docs/plugins#access-plugin) and [runtime hooks](/docs/runtime-hooks) instead.
* Secrets must never appear in prompt files or YAML.
