Skip to content

System Prompts ​

The system prompt tells Iris who it is and what it knows about you. It's assembled dynamically for each request, combining a static persona with contextual information like memories, summaries, and calendar events.

How It Works ​

Every time you send a message, Iris builds a system prompt by rendering a series of prompt classes in order. Each class is responsible for one section of the prompt:

┌─ Cached Group (1h TTL) ─────────────────────┐
│  IrisStaticPrompt           ← Identity       │
│  AutonomousExecutionPrompt  ← Agent behavior  │
│  SkillsPrompt               ← Skills  (BP #1) │
└──────────────────────────────────────────────┘
┌─ Cached Group (ephemeral) ──────────────────┐
│  PinnedSkillsPrompt         ← Pinned skills   │
│  PinnedPromptsPrompt        ← Pinned prompts  │
│  SummaryPrompt              ← Summaries        │
│  CrossThreadContextPrompt   ← Briefs  (BP #2) │
└──────────────────────────────────────────────┘
  MemoryPrompt                ← Truths + memories
  CalendarPrompt              ← Upcoming events
  WeatherPrompt               ← Current conditions
  CurrentTimePrompt           ← Current date/time

Prompts are organized into cache breakpoint groups to optimize token costs with Anthropic's prompt caching. Cached groups share a single cache breakpoint, while standalone prompts below the groups contain dynamic, per-request content that isn't cached.

The result is a personalized, context-aware prompt that includes everything Iris needs to respond appropriately.

The heartbeat system uses a separate, purpose-built prompt stack. See Heartbeat Prompt Stack for details.

Architecture ​

Prompts are self-contained: each prompt injects a RequestContext and any services it needs, then fetches its own context when content() is called. This provides:

  • Uniform pattern: Core and custom prompts work identically
  • Testability: Prompts can be tested in isolation
  • Flexibility: Each prompt injects only what it needs
  • Conditional rendering: Prompts can return empty content if they have nothing to contribute

Prompt Pipeline ​

Each prompt is a separate class rendered in order. The default pipeline organizes prompts into cache breakpoint groups — cached groups for stable content and standalone entries for dynamic content:

Cached Group 1: Static Content (1h TTL) ​

These three prompts form the first cache group. Their content rarely changes, so they share a single cache breakpoint with a 1-hour TTL.

Static Prompt — Core identity and behavior:

  • Identity and personality
  • Communication style
  • Tool usage guidelines

Autonomous Execution Prompt — Behavioral guidance for autonomous tool usage:

  • Iterate on errors instead of stopping after one failure
  • Verify results before claiming success
  • Execute multi-step plans without asking permission between steps
  • Clear escalation criteria for when to ask vs. keep going

This applies to all tools — shell, filesystem, memory, calendar, etc. Tool-specific mechanics (path conventions, read-before-edit gates) live in the tool descriptions themselves.

Skills Prompt — Lists available agent skills for Iris to activate:

  • Skill names and descriptions from .agents/skills/
  • Only renders when skills are enabled and available
  • Content is loaded from disk files, not generated per-request

Cached Group 2: Pinned & Thread Context (Ephemeral) ​

These prompts form the second cache group with the default 5-minute TTL. Their content changes more often than core identity — when you pin or unpin skills, when summaries generate, or when thread briefs update — so they use a shorter cache lifetime.

Pinned Skills Prompt — Injects the full content of skills pinned to the active thread. Unlike the Skills Prompt above (which lists skill names for on-demand activation), this prompt loads each pinned skill's complete instructions into the system prompt so they're always active.

  • Reads pinned skill names from the thread's settings
  • Loads each skill's full content via the skill loader
  • Each skill becomes a separate system message
  • Only renders when the active thread has pinned skills

Pinned Prompts Prompt — Injects custom prompts pinned to the thread.

Because pinned content is loaded in full, it consumes context window space on every message. The thread settings modal shows a context consumption notice so you can see how much space your pinned skills use. If a pinned skill has been removed from disk, Iris shows a warning in the thread settings modal.

Summary Prompt — Injects recent conversation summaries for continuity:

  • Narrative arc from previous conversations
  • Emotional context and relationship dynamics
  • Only renders when summaries exist

Cross-Thread Context Prompt — Injects thread briefs from other active threads:

  • Briefs from the most recently active threads (up to 5, configurable)
  • Current thread is always excluded
  • Static timestamps for cache efficiency
  • Only renders when other threads have briefs and iris.briefs.enabled is true

TIP

When no skills are pinned and no briefs or summaries exist, the prompts in this group return empty content. The group doesn't consume a cache breakpoint in that case — it's as if it doesn't exist.

Memory Prompt ​

Recalls relevant context for the current message:

  • Truths: Stable, core facts ranked by relevance (pinned Truths always included)
  • Memories: Semantically similar memories found via search
  • Only renders when Truths or memories exist

Calendar Prompt ​

Retrieves upcoming calendar events for the current user:

  • Upcoming events for the next 7 days
  • Calendar names and default calendar info
  • Only renders when events exist

Weather Prompt ​

Provides current weather conditions for the user's configured location. Only renders when weather data is available.

Current Time Prompt ​

Provides the current date and time in the user's timezone (falling back to the configured default). Always renders.

Heartbeat Prompt Stack ​

The heartbeat system uses a dedicated prompt stack configured separately from the conversation prompts. The heartbeat needs Iris's personality for consistent voice, but doesn't need conversation-scoped prompts like pinned skills, autonomous execution guidance, or thread summaries.

┌─ Cached Group (ephemeral) ──────────────────┐
│  PersonaPrompt              ← Voice  (BP #1) │
└──────────────────────────────────────────────┘
  HeartbeatConversationContextPrompt ← Thread briefs + summaries
  MemoryPrompt                       ← Truths + memories
  CalendarPrompt                     ← Upcoming events
  WeatherPrompt                      ← Current conditions
  CurrentTimePrompt                  ← Current date/time

PersonaPrompt — The core Iris persona extracted as a standalone prompt. This gives the heartbeat a consistent voice when crafting proactive messages without the full IrisStaticPrompt (which includes tool invocation protocols and other conversation-specific guidance).

HeartbeatConversationContextPrompt — Provides awareness of active conversations. For each of the user's most recently active threads (up to heartbeat.context_max_threads), it includes:

  • Thread name and last activity timestamp
  • Thread brief (if available)
  • Latest conversation summary with narrative thread, emotional state, unresolved threads, and active goals

This gives the heartbeat enough context to decide whether to reach out based on what's happening across conversations — without the full conversation prompt stack.

The heartbeat prompt stack is configured via iris.heartbeat.prompts and uses the same format as the main iris.prompts config (cache groups and standalone entries). The SystemPromptBuilder reads from this alternate config key when building heartbeat system messages.

Prompt Templates ​

Templates are Blade files in resources/views/prompts/:

prompts/
├── personas/
│   └── iris-static.blade.php                # Core identity + protocols
├── persona.blade.php                        # Standalone persona definition
├── recalled-context.blade.php               # Memory context
├── calendar-context.blade.php               # Calendar events
├── weather-context.blade.php                # Weather conditions
├── summary-context.blade.php                # Conversation summaries
├── cross-thread-context.blade.php           # Thread briefs
└── heartbeat-conversation-context.blade.php # Thread briefs + summaries (heartbeat)

Customizing Prompts ​

Create your own prompt classes to customize Iris's behavior. Each prompt is self-contained and injects the dependencies it needs.

The content() method can return either a string or a Blade view:

php
<?php

declare(strict_types=1);

namespace App\Extensions\Prompts;

use App\Prompts\Prompt;
use App\ValueObjects\RequestContext;
use Illuminate\View\View;

class WeatherPrompt extends Prompt
{
    public function __construct(
        protected RequestContext $requestContext,
        protected WeatherService $weather,
    ) {}

    // Return a Blade view for complex templates
    public function content(): View
    {
        return view('prompts.extensions.weather', [
            'forecast' => $this->weather->getForecast(
                $this->requestContext->user()?->id
            ),
        ]);
    }
}

For simpler prompts, return a string directly:

php
<?php

declare(strict_types=1);

namespace App\Extensions\Prompts;

use App\Prompts\Prompt;

class TimezonePrompt extends Prompt
{
    public function content(): string
    {
        return "## User Timezone\n\nThe user is in the Pacific timezone.";
    }
}

The RequestContext API ​

The RequestContext value object provides access to everything about the current request:

MethodReturnsDescription
user()User|nullThe authenticated user model, or null if unauthenticated
thread()Thread|nullThe active thread for this request
message()stringThe user's message text for this request
images()arrayArray of attached image data (for multi-modal requests)

Using RequestContext ​

Every prompt receives a RequestContext via constructor injection. Use it to customize prompt content based on the current request:

php
public function __construct(
    protected RequestContext $requestContext,
) {}

public function content(): string
{
    $user = $this->requestContext->user();

    if (! $user) {
        return ''; // No content for unauthenticated requests
    }

    // Customize based on user or message
    $message = $this->requestContext->message();

    if (str_contains(strtolower($message), 'urgent')) {
        return "## Priority Mode\n\nThe user has indicated urgency.";
    }

    return '';
}

Registering Custom Prompts ​

Add your prompt to config/iris-custom.php. The prompts array replaces the default list entirely, so include the core prompts and cache groups you want to keep:

php
// config/iris-custom.php
return [
    'prompts' => [
        // Cached group — static content with 1h TTL
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\IrisStaticPrompt::class,
                App\Prompts\AutonomousExecutionPrompt::class,
                App\Prompts\SkillsPrompt::class,
            ],
        ],
        // Cached group — pinned content, summaries, and cross-thread context
        [
            'cache' => ['type' => 'ephemeral'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
                App\Prompts\SummaryPrompt::class,
                App\Prompts\CrossThreadContextPrompt::class,
            ],
        ],
        // Dynamic, per-request content (no caching)
        App\Prompts\MemoryPrompt::class,
        App\Extensions\Prompts\WeatherPrompt::class,  // Your custom prompt
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

Dynamic custom prompts go as standalone entries after the cached groups. If your prompt is static content that rarely changes, consider adding it to an existing cached group instead. See Cache Breakpoints for more on grouping strategies.

Prompt Ordering Considerations ​

Prompts are rendered in order, and that order matters. The system prompt flows from cached groups of stable content to standalone dynamic entries:

  1. Cached Group 1 — Static identity and skills: Establishes who Iris is, how it behaves, and what skills are available. Cached with a 1h TTL because this content rarely changes.
  2. Cached Group 2 — Pinned content, summaries, and cross-thread context: Thread-specific skills and prompts, conversation summaries, and thread briefs from other active conversations. Cached with the default 5m TTL since this content can change mid-session but doesn't change per-request.
  3. Memories (standalone): Facts about the user that inform the response — changes per request.
  4. Integrations (standalone): Calendar, weather, or other external data — changes per request.
  5. Temporal context last (standalone): Current date/time anchors everything — always changes.

This ordering maximizes prompt cache hit rates. Because Anthropic's caching is prefix-based, stable content at the top gets cached while dynamic content at the bottom changes per-request without invalidating the cache. See Cache Breakpoints for the full details on how groups map to breakpoints.

When adding custom prompts, consider where the information fits logically. Static content that rarely changes should go in a cached group (or be added to an existing one). Dynamic, per-request content like project status or user-specific context should be standalone entries, typically placed after memories but before temporal context.

Complete Custom Prompt Example ​

Here's a full example of creating and registering a custom prompt that injects work project context:

1. Create the Prompt Class ​

php
<?php

declare(strict_types=1);

namespace App\Extensions\Prompts;

use App\Models\User;
use App\Prompts\Prompt;
use App\Services\ProjectService;
use App\ValueObjects\RequestContext;
use Illuminate\View\View;

class WorkContextPrompt extends Prompt
{
    public function __construct(
        protected RequestContext $requestContext,
        protected ProjectService $projectService,
    ) {}

    public function content(): View|string
    {
        $user = $this->requestContext->user();

        if (! $user) {
            return '';
        }

        $projects = $this->projectService->getActiveProjects($user->id);

        if ($projects->isEmpty()) {
            return '';
        }

        return view('prompts.extensions.work-context', [
            'projects' => $projects,
        ]);
    }
}

2. Create the Blade Template ​

blade
{{-- resources/views/prompts/extensions/work-context.blade.php --}}
## Current Work Projects

The user is currently working on these projects:

@foreach($projects as $project)
- **{{ $project->name }}**: {{ $project->description }}
  - Status: {{ $project->status }}
  - Deadline: {{ $project->deadline?->format('F j, Y') ?? 'No deadline' }}
@endforeach

Use this context when the user asks about work or projects.

3. Register the Prompt ​

php
// config/iris-custom.php
return [
    'prompts' => [
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\IrisStaticPrompt::class,
                App\Prompts\AutonomousExecutionPrompt::class,
                App\Prompts\SkillsPrompt::class,
            ],
        ],
        [
            'cache' => ['type' => 'ephemeral'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
                App\Prompts\SummaryPrompt::class,
                App\Prompts\CrossThreadContextPrompt::class,
            ],
        ],
        App\Prompts\MemoryPrompt::class,
        App\Extensions\Prompts\WorkContextPrompt::class,  // After memories
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

The WorkContextPrompt is dynamic (fetches active projects per request), so it goes as a standalone entry outside any cached group.

Caching Static Prompts ​

Prompt caching is managed through cache breakpoint groups in your config, not in individual prompt classes. To cache a prompt, place it inside a group with a cache key in the prompts config:

php
// config/iris-custom.php
'prompts' => [
    [
        'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
        'prompts' => [
            App\Prompts\IrisStaticPrompt::class,
            App\Extensions\Prompts\YourStaticPrompt::class,  // Cached with the group
        ],
    ],
    App\Prompts\MemoryPrompt::class,  // Not cached (dynamic content)
],

Individual prompt classes don't need to set providerOptions() for caching — any cacheType or cacheTtl values set by prompts are automatically stripped. The config is the single authority for where cache breakpoints are placed. See Cache Breakpoints for the full guide on configuring groups, TTL options, and the 4-breakpoint limit.