System Prompt Templates
Dynamic variables and embedded queries in system prompts
System prompts support a template syntax that injects dynamic data at runtime. This allows agents to receive up-to-date context about the organization, current time, available entity types, and even live query results directly in their system prompt.
Template Syntax
Templates use double curly braces: {{variableName}}. Variables are resolved when the agent processes a message, before the system prompt is sent to the LLM.
import { defineAgent } from 'struere'
export default defineAgent({
name: "Support Agent",
slug: "support",
version: "0.1.0",
systemPrompt: `You are {{agentName}}, an assistant for {{organizationName}}.
Current time: {{currentTime}}
Available entity types: {{entityTypes}}
Use entity.query to search for entities by type.`,
model: { provider: "anthropic", name: "claude-sonnet-4" },
tools: ["entity.query", "entity.get", "event.emit"],
})
Available Variables
| Variable | Type | Description |
|---|---|---|
{{currentTime}} |
string |
ISO 8601 timestamp (e.g., "2025-03-15T14:30:00.000Z") |
{{datetime}} |
string |
ISO 8601 timestamp (alias for currentTime) |
{{timestamp}} |
number |
Unix timestamp in milliseconds |
{{organizationName}} |
string |
The organization's display name |
{{organizationId}} |
string |
The Convex organization ID |
{{agentName}} |
string |
The agent's display name |
{{agent.name}} |
string |
The agent's display name (dotted access) |
{{agent.slug}} |
string |
The agent's slug identifier |
{{userId}} |
string |
The current user's ID (if applicable) |
{{threadId}} |
string |
The current conversation thread ID |
{{message}} |
string |
The current user message being processed |
{{thread.metadata.X}} |
any |
Access thread metadata field X (replace X with the field name) |
{{entityTypes}} |
array |
JSON array of all entity types in the current environment |
{{roles}} |
array |
JSON array of all roles in the current environment |
Variable Resolution
Variables support dot notation for nested access. The template engine walks the context object following each dot-separated segment:
{{agent.name}}resolves tocontext.agent.name{{thread.metadata.customerId}}resolves tocontext.thread.metadata.customerId
If a variable resolves to an object or array, it is serialized as JSON. If a variable cannot be resolved, the template outputs [TEMPLATE_ERROR: variableName not found].
entityTypes Structure
The {{entityTypes}} variable resolves to a JSON array of entity type objects:
[
{
"name": "Teacher",
"slug": "teacher",
"description": "Tutors who conduct sessions",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"hourlyRate": { "type": "number" }
}
},
"searchFields": ["name", "email"]
},
{
"name": "Student",
"slug": "student",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"grade": { "type": "string" }
}
},
"searchFields": ["name"]
}
]
This gives agents full awareness of the data model so they can construct valid entity.query and entity.create calls.
Function Calls (Embedded Queries)
Templates can embed live queries that execute at prompt-resolution time. Function calls use the same double curly brace syntax with parentheses:
{{functionName({"key": "value"})}}
entity.query
Queries entities by type and injects the results into the system prompt:
{{entity.query({"type": "teacher", "limit": 5})}}
This resolves to a JSON array of entity objects, filtered through the agent's permissions (scope rules and field masks apply).
entity.get
Retrieves a single entity by type and ID:
{{entity.get({"type": "customer", "id": "ent_abc123"})}}
Nested Templates
Function arguments can contain template variables, enabling dynamic queries based on thread context:
{{entity.get({"type": "customer", "id": "{{thread.metadata.customerId}}"})}}
In this example:
{{thread.metadata.customerId}}is resolved first to the actual customer ID- The resolved ID is then used as the argument to
entity.get - The entity data is fetched and injected into the system prompt
This is particularly useful for agents that need context about a specific entity associated with the current conversation thread.
Unsupported Syntax
Handlebars block helpers are not supported. The following will not work:
{{#each entityTypes}}
- {{this.name}}
{{/each}}
{{#if userId}}
User is logged in.
{{/if}}
Instead, use the raw variable which returns the JSON representation:
Available types: {{entityTypes}}
The LLM can parse the JSON array directly.
Result Truncation
Function call results are truncated to 10 KB to prevent excessively large system prompts. If a result exceeds this limit, it is cut off with a ...[truncated] suffix.
Error Handling
If a function call fails, the template engine replaces it with an error marker:
| Error | Output |
|---|---|
| Invalid JSON arguments | [TEMPLATE_ERROR: entity.query - invalid JSON arguments] |
| Permission denied | [] (empty array) |
| Tool not found | [TEMPLATE_ERROR: toolName - tool not found] |
| Execution failure | [TEMPLATE_ERROR: toolName - error message] |
Permission errors produce empty results rather than error messages, so agents gracefully degrade when they lack access to certain data.
Full Example
A scheduling agent with rich context injection:
import { defineAgent } from 'struere'
export default defineAgent({
name: "Scheduler",
slug: "scheduler",
version: "0.1.0",
systemPrompt: `You are {{agentName}}, a scheduling assistant for {{organizationName}}.
Current time: {{currentTime}}
## Available Entity Types
{{entityTypes}}
## Current Teachers
{{entity.query({"type": "teacher"})}}
## Current Students
{{entity.query({"type": "student"})}}
## Instructions
- Use the entity types above to understand the data schema
- Query sessions with entity.query to check for conflicts
- Create new sessions with entity.create
- Always verify teacher availability before booking
- Sessions must be booked at least 24 hours in advance`,
model: {
provider: "anthropic",
name: "claude-sonnet-4",
temperature: 0.3,
},
tools: [
"entity.create",
"entity.get",
"entity.query",
"entity.update",
"event.emit",
],
})
A customer-support agent that loads the customer's profile from thread metadata:
import { defineAgent } from 'struere'
export default defineAgent({
name: "Customer Support",
slug: "customer-support",
version: "0.1.0",
systemPrompt: `You are a support agent for {{organizationName}}.
## Customer Profile
{{entity.get({"type": "customer", "id": "{{thread.metadata.customerId}}"})}}
## Recent Events
{{entity.query({"type": "ticket", "filters": {"customerId": "{{thread.metadata.customerId}}"}, "limit": 10})}}
Help the customer with their request. You have their profile and recent tickets loaded above.`,
model: { provider: "anthropic", name: "claude-sonnet-4" },
tools: ["entity.query", "entity.update", "event.emit"],
})