Events
Event definitions and versioning
Events
An Event in Narrative Protocol defines something that can happen in your world. Events are the actions, triggers, or occurrences that drive state changes in your simulation.
What is an Event?
Events describe behavior, not data. They answer the question: "What can happen in this world?"
Examples:
race_result- A horse race is completedtraining_session- A horse undergoes traininginjury_occurred- A horse gets injuredweather_change- Weather conditions shift
Event Structure
Each event belongs to a world and has:
| Field | Description |
|---|---|
name | Unique identifier within the world |
description | Human-readable explanation |
versions | One or more versioned implementations |
Creating an Event
Events are always created with their first version:
POST /api/worlds/:worldAddress/events{
"name": "race_result",
"description": "Resolves a race and updates horse statistics",
"firstVersion": {
"inputSchema": { "raceId": "string" },
"readEntities": [{ "schema": "horse" }, { "schema": "jockey" }],
"stateChangeSchema": { "horse": "partial" },
"outputSchema": { "winner": "string", "time": "string" },
"mutationSettings": {
"mode": "ai",
"behaviorPrompt": "Determine the race winner based on horse stats."
},
"executionSettings": { "visibility": "admin" }
}
}Event Versioning
Events have versions that become immutable once locked. This ensures reproducibility and auditability of state changes over time.
Why Versioning?
Event behavior may need to change over time:
- Fix bugs in behavior prompts
- Add new input fields
- Adjust state change logic
Without versioning, changing an event would make historical executions impossible to reproduce.
Version Lifecycle
Version 1 (draft)
│
▼ [lock]
Version 1 (locked) ─── immutable, bound to deployments
│
▼ [create new version]
Version 2 (draft)
│
▼ [lock]
Version 2 (locked) ─── immutable, can be boundDraft State
New versions start as drafts. You can modify:
inputSchema- What data the event acceptsreadEntities- Which entity schemas to readstateChangeSchema- How state changes (partial/full/append)outputSchema- What gets returned publiclymutationSettings- Execution mode and settings (AI prompt or direct template)
Locked State
Once locked via POST /api/worlds/:worldAddress/events/:eventName/event-versions/:version/lock:
- Version becomes immutable
- Can be bound to deployments
- Cannot be deleted
Irreversible Action
Locking a version is permanent. Once locked, the version cannot be modified or deleted. Always test drafts thoroughly before locking.
Version Fields
| Field | Type | Description |
|---|---|---|
inputSchema | object | Expected input fields and types |
readEntities | object[] | Entity schema read config (schema, optional filter) |
stateChangeSchema | object | Per-schema change mode (partial, full, append) |
outputSchema | object | Public output structure |
mutationSettings | object | Execution mode settings: ai (with behaviorPrompt) or direct (with template) |
executionSettings | object | Visibility: admin, public, or cron (+ price/cron config) |
readEntities and Filter
The readEntities field in event versions specifies which entity instances to load from the world state. The filter array tells the system which specific entity instance IDs to load from the event input.
Filter Syntax
filter is an array of input paths that tells the system which specific entity instance IDs to load from the event input (e.g., ["raceId"] looks up eventInput.raceId to find the target instance).
Examples
1. Simple input key
Event input:
{ "raceId": "RACE_001" }readEntities config:
[
{ "schemaId": 1, "filter": ["raceId"] }
]Loads instances where instanceId === "RACE_001"
2. Nested input key
Event input:
{ "data": { "raceId": "RACE_001" } }readEntities config:
[
{ "schemaId": 1, "filter": ["data.raceId"] }
]Uses dot notation to access nested values
3. Multiple filters (OR logic)
Event input:
{ "horse1": "HORSE_1", "horse2": "HORSE_2" }readEntities config:
[
{ "schemaId": 1, "filter": ["horse1", "horse2"] }
]Loads both HORSE_1 and HORSE_2 instances
4. Schema-prefixed instance IDs
If your instance IDs are prefixed with schema name (e.g., "horse:HORSE_1"), use the schema name in the filter:
Event input:
{ "winnerId": "horse:HORSE_1" }readEntities config:
[
{ "schemaId": 1, "filter": ["winnerId"] }
]Matches instanceId === "horse:HORSE_1" exactly
5. Multiple schemas with different filters
Event input:
{ "raceId": "RACE_001", "betId": "BET_001" }readEntities config:
[
{ "schemaId": 1, "filter": ["raceId"] },
{ "schemaId": 2, "filter": ["betId"] }
]Schema 1 loads race instances, schema 2 loads bet instances
6. No filter (load all instances of schema)
[
{ "schemaId": 1 }
]Loads all instances of schema 1 (useful for listing queries)
Mutation Settings
mutationSettings controls how entity states are mutated during execution.
State mutation always ends as runtime stateChanges, and the version stateChangeSchema map decides how those changes apply (partial, full, append).
Mode-Specific Requirements
- When
mutationSettings.modeis"ai":behaviorPromptis required,templatemust not be provided - When
mutationSettings.modeis"direct":templateis required,behaviorPromptmust not be provided
AI Mutation (mode: "ai")
In AI mode, the engine builds execution context from:
- world definition (
name,description,domainTags,promptSeed) - event version config (
inputSchema,readEntities,behaviorPrompt,outputSchema) - current deployment state (filtered by
readEntities) - runtime input
The model returns structured stateChanges and result, then the system validates and applies the changes.
Use AI mode when mutation logic depends on interpretation, simulation, or generative reasoning.
Direct Mutation (mode: "direct")
In direct mode, execution does not rely on LLM reasoning for mutation. The system resolves a deterministic mutationSettings.template and produces stateChanges/result directly.
Use direct mode when:
- updates should be deterministic and explainable from input/template
- you want lower variance than prompt-based execution
- event logic is mostly transform/map/set operations
Direct Mode Requirement
mutationSettings.template is required when mode is direct.
Example (direct mutation):
{
"inputSchema": { "winner": "string" },
"outputSchema": { "winner": "string" },
"readEntities": [{ "schema": "horse" }],
"stateChangeSchema": { "horse": "partial" },
"mutationSettings": {
"mode": "direct",
"template": {
"stateChanges": {
"horse:{{input.winner}}": { "wins": { "$increment": 1 } }
},
"result": {
"winner": "{{input.winner}}"
}
}
},
"executionSettings": { "visibility": "admin" }
}Template Engine (Direct + Cron)
Direct mutation templates and cron input templates use the same resolver.
Template syntax:
{{input.someKey}}from execution input{{walletAddress}}wallet executing public app route (if present){{timestamp}}execution timestamp (ISO string)- Cron context variables (available in cron-triggered events):
{{date}}— Current date in ISO format (e.g., "2024-01-15"){{unix}}— Current Unix timestamp in seconds{{counter}}— Auto-incrementing counter that persists between cron runs{{cronState.key}}— Access custom state stored from previous executions
Type behavior:
- If a value is exactly
{{...}}, the resolver preserves primitive type when possible (for example number). - If
{{...}}appears inside a larger string, it is interpolated as text.
Operator support (applied against current instance state):
$set: set value directly (replaces entire field)$merge: merge object into existing object field (preserves existing keys)$increment: increment number (defaults to+1if value omitted/non-numeric)$append: append item to array
Direct template shape:
{
"horse:HORSE_1": {
"wins": { "$increment": 1 },
"lastWinnerWallet": { "$set": "{{walletAddress}}" },
"raceLog": { "$append": "{{input.raceId}}" }
}
}Cron Context Example
When executing events via cron, you can use all the context variables to track state across scheduled executions:
{
"executionSettings": {
"visibility": "cron",
"cron": {
"schedule": "0 * * * *",
"input": {
"currentTime": "{{unix}}",
"currentDate": "{{date}}",
"runNumber": "{{counter}}",
"previousRun": "{{cronState.counter}}"
}
},
"mutations": {
"cronTracker:DAILY": {
"totalRuns": { "$set": "{{counter}}" },
"lastRunTime": { "$set": "{{unix}}" },
"previousCounter": { "$set": "{{cronState.counter}}" }
}
}
}
}The counter increments automatically on each cron execution and persists across runs. Use {{cronState.counter}} to access the previous counter value, or store custom state using any key name that will be available in cronState for the next execution.
Notes:
- Top-level keys should be
schemaName:instanceId. - Missing instances can be auto-created during direct execution.
- Input is validated before resolution (size/depth limits and blocked patterns).
Execution Settings
executionSettings controls who can run an event version and when it can run.
Concurrent Execution Lock
Events use a mutex (mutual exclusion) lock to prevent race conditions when multiple executions target the same entity data. An event cannot execute concurrently if:
- Same event - The identical event is already executing on the deployment
- Shared entity schema - Any entity schema is both read by the new event and modified by a running event (via
readEntitiesorstateChanges)
// Lock prevents concurrent execution if:
eventA.readEntities ∩ eventB.stateChanges ≠ ∅This ensures data consistency when events modify overlapping entities. If a conflicting execution is in progress, the new execution request will fail.
Visibility Modes
| Value | Meaning |
|---|---|
admin | API-key authenticated execution via /api/worlds/:worldAddress/deployments/:deploymentAddress/execute |
public | Wallet-signed execution via /app/deployments/:address/execute |
cron | Scheduled execution by the background cron executor |
Public Execution Details
When visibility is public:
- deployment must be public
- caller must provide
wallet,signature,message - if
priceUsd > 0, caller must provide a valid x402X-Paymentheader
Example (public with price):
{
"executionSettings": {
"visibility": "public",
"priceUsd": 0.5
}
}Caller executes via:
POST /app/deployments/:address/executeCron Execution Details
When visibility is cron:
- set
executionSettings.cron.schedule(valid cron expression) - optionally set
executionSettings.cron.inputtemplate - executor runs periodically and triggers due events automatically
Example (cron visibility):
{
"executionSettings": {
"visibility": "cron",
"cron": {
"schedule": "*/5 * * * *",
"input": {
"tick": "{{counter}}",
"timestamp": "{{timestamp}}"
}
}
}
}Cron input template example:
{
"executionSettings": {
"visibility": "cron",
"cron": {
"schedule": "0 * * * *",
"input": {
"tick": "{{counter}}",
"runAt": "{{timestamp}}",
"day": "{{date}}",
"lastCounter": "{{cronState.counter}}"
}
}
}
}State Change Modes
The stateChangeSchema maps entity schema names to change modes:
{
"horse": "partial",
"race_record": "append"
}| Mode | Behavior |
|---|---|
partial | Merge changes into existing state (default) |
full | Replace entire state |
append | Append to arrays, merge objects |
Binding Versions to Deployments
Deployments bind to specific versions. Multiple deployments can use different versions:
Event: "race_result"
├── Version 1 (locked) ─── bound to "Production"
├── Version 2 (locked) ─── bound to "Staging"
└── Version 3 (draft) ─── not yet lockedCreating a New Version
POST /api/worlds/:worldAddress/events/:eventName/event-versions{
"inputSchema": { "raceId": "string", "weather": "string" },
"readEntities": [{ "schema": "horse" }, { "schema": "jockey" }],
"stateChangeSchema": { "horse": "partial" },
"mutationSettings": {
"mode": "ai",
"behaviorPrompt": "Consider weather conditions when determining the winner."
}
}Version numbers auto-increment based on existing versions.
Best Practices
Version Management Tips
- Test drafts thoroughly before locking
- Document changes in version descriptions
- Use staging deployments to test new versions
- Never rely on modifying locked versions - create new ones instead
API Reference
POST /api/worlds/:worldAddress/events- Create event with first versionPOST /api/worlds/:worldAddress/events/:eventName/event-versions- Create additional versionsPOST /api/worlds/:worldAddress/events/:eventName/event-versions/:version/lock- Lock (make immutable)
Related Concepts
- AI Engine - How events are processed
- Deployments - Where events are bound and executed