Technical Specification

EventID Specification v1.0

The complete technical specification for the Universal Event Model's EventID format. Formal grammar, semantic rules, validation requirements, and reference implementations.

EventID Format

Every event in the Universal Event Model is identified by a structured EventID that follows a strict format. This format is designed to be human-readable, machine-parseable, and self-documenting.

EventID Format
domain.entity.action:version // Example: auth.user.login:1

Domain

The system or bounded context where the event originates.

auth payments agent

Entity

The specific object or resource the event relates to.

user order code

Action

What happened - the specific state change or activity.

login charged generated

Version

Schema version for backward compatibility.

1 2 3

Formal Grammar

The EventID format follows a strict grammar defined in Extended Backus-Naur Form (EBNF). All parsers and validators must implement this grammar exactly.

(* Syntax.ai - Universal Event Model Grammar *) (* EBNF Notation *) event_id = domain , "." , entity , "." , action , ":" , version ; domain = identifier ; entity = identifier ; action = identifier ; version = positive_integer ; identifier = lowercase , { lowercase | digit | "_" } ; positive_integer = nonzero_digit , { digit } ; lowercase = "a" | "b" | ... | "z" ; digit = "0" | "1" | ... | "9" ; nonzero_digit = "1" | "2" | ... | "9" ;

Regular Expression

For quick validation, use this regex pattern:

Regex Pattern
^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*:[1-9][0-9]*$ // Breakdown: // ^ - Start of string // [a-z][a-z0-9_]* - Domain (lowercase start, then lowercase/digit/underscore) // \. - Literal dot separator // [a-z][a-z0-9_]* - Entity (same rules as domain) // \. - Literal dot separator // [a-z][a-z0-9_]* - Action (same rules) // : - Literal colon separator // [1-9][0-9]* - Version (positive integer, no leading zeros) // $ - End of string

Semantic Rules

Beyond syntactic validity, EventIDs must follow semantic rules to ensure consistency and usability across systems.

Rule 1: Lowercase Only

All identifiers (domain, entity, action) must be lowercase. No uppercase letters are permitted.

auth.user.login:1

Rule 2: Start with Letter

Identifiers must begin with a lowercase letter (a-z), not a number or underscore.

user2 not 2user

Rule 3: Positive Version

Version must be a positive integer (1 or greater). Zero and negative numbers are invalid.

:1 :42 not :0

Rule 4: No Leading Zeros

Version numbers cannot have leading zeros. Use 1 not 01.

:1 not :01

Rule 5: Underscores Allowed

Underscores can be used within identifiers to separate words, but not at the start.

user_account not _user

Rule 6: No Empty Parts

All four components are required. Empty strings are not permitted for any part.

a.b.c:1 minimum

Character Set

Component Allowed Characters First Character Length
Domain a-z 0-9 _ a-z 1+ characters
Entity a-z 0-9 _ a-z 1+ characters
Action a-z 0-9 _ a-z 1+ characters
Version 0-9 1-9 1+ digits

Examples

Reference examples for valid and invalid EventIDs to guide implementation and testing.

Valid EventIDs
auth.user.login:1 Basic format
payments.order.charged:2 Version 2
agent.code.generated:1 AI domain
user2.account_v2.reset:42 Numbers & underscores
a.b.c:1 Minimum valid
system.health.checked:999 Large version
Invalid EventIDs
Auth.User.Login:1 Uppercase not allowed
auth.user:1 Missing action
auth.user.login:0 Version must be >= 1
auth.user.login:01 No leading zeros
2auth.user.login:1 Must start with letter
auth.user.login Missing version

Edge Cases & Guidelines

Real-world scenarios that require careful consideration when designing event schemas.

Sub-entity Actions

When an action involves a sub-entity (like billing for an account), should you nest the structure or keep it flat?

// Option A: Nested entity (NOT VALID - only 3 parts allowed) account.billing.payment.processed:1 // Invalid - 4 parts // Option B: Compound entity (RECOMMENDED) account.billing_payment.processed:1 // Valid // Option C: Compound action account.billing.payment_processed:1 // Valid
Recommendation: Use compound identifiers with underscores to maintain the three-part structure while preserving semantic clarity.

Entity State vs. Entity Action

Distinguishing between state changes and discrete actions on the same entity.

// State change events (past tense) orders.order.created:1 orders.order.shipped:1 orders.order.cancelled:1 // Action events (present/active) orders.order.refund_requested:1 orders.order.review_submitted:1
Recommendation: Use past tense for state changes (created, updated, deleted) and compound forms for action + result combinations (refund_requested, review_submitted).

Versioning Strategy

When to increment the version number versus creating a new event type.

// Schema evolution - same event, new version auth.user.login:1 // Original: { user_id } auth.user.login:2 // Added: { user_id, ip_addr, device } // Semantic change - new event type auth.user.login:1 // Password login auth.user.login_sso:1 // SSO login (different flow) auth.user.login_biometric:1 // Biometric (different payload)
Recommendation: Increment version for additive, backward-compatible changes. Create new event types for semantically different operations or breaking changes.

AI/Agent Events

Standard patterns for AI-generated code and agent activity events.

// Code generation events agent.code.generated:1 // AI wrote code agent.code.modified:1 // AI modified code agent.code.reviewed:1 // AI reviewed code // Testing events agent.test.created:1 // AI created test agent.test.executed:1 // Test was run agent.test.passed:1 // Test passed agent.test.failed:1 // Test failed // Feedback loop events agent.iteration.started:1 agent.iteration.completed:1 agent.iteration.failed:1
Recommendation: Use the agent domain for all AI-generated activity. This makes it trivial to query, audit, and analyze all AI behavior in your system.

Reference Implementations

Production-ready implementations for common platforms. All implementations follow the specification exactly.

Rust
PostgreSQL
TypeScript
lib.rs - Syntax.ai Event Model
//! Syntax.ai - Universal Event Model //! Make AI-Generated Code Observable. Verifiable. Improvable. use std::str::FromStr; use serde::{Serialize, Deserialize}; use regex::Regex; use once_cell::sync::Lazy; static EVENT_ID_REGEX: Lazy<Regex> = Lazy::new(|| { Regex::new(r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*:[1-9][0-9]*$") .expect("Invalid regex") }); #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct EventId { pub domain: String, pub entity: String, pub action: String, pub version: u32, } impl FromStr for EventId { type Err = EventIdError; fn from_str(s: &str) -> Result<Self, Self::Err> { if !EVENT_ID_REGEX.is_match(s) { return Err(EventIdError::InvalidFormat(s.to_string())); } let (path, version_str) = s.split_once(':') .ok_or_else(|| EventIdError::InvalidFormat(s.to_string()))?; let parts: Vec<&str> = path.split('.').collect(); Ok(EventId { domain: parts[0].to_string(), entity: parts[1].to_string(), action: parts[2].to_string(), version: version_str.parse()?, }) } } impl EventId { /// Match against a pattern with % wildcards pub fn matches_pattern(&self, pattern: &str) -> bool { let self_str = self.to_string(); let regex_pattern = format!( "^{}$", regex::escape(pattern).replace("%", ".*") ); Regex::new(&regex_pattern) .map(|re| re.is_match(&self_str)) .unwrap_or(false) } }

Full implementation available at rust-example/ - includes emit! macro, Event struct, and async EventCollector.

schema.sql - PostgreSQL Schema
-- Syntax.ai - Universal Event Model PostgreSQL Schema -- Make AI-Generated Code Observable. Verifiable. Improvable. -- Domain type for validated EventIDs CREATE DOMAIN event_id AS TEXT CHECK ( VALUE ~ '^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*:[1-9][0-9]*$' ); -- Events table with automatic component extraction CREATE TABLE events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id event_id NOT NULL, timestamp TIMESTAMPTZ NOT NULL DEFAULT now(), payload JSONB NOT NULL DEFAULT '{}', -- Extracted components for efficient querying domain TEXT GENERATED ALWAYS AS ( split_part(event_id::TEXT, '.', 1) ) STORED, entity TEXT GENERATED ALWAYS AS ( split_part(split_part(event_id::TEXT, ':', 1), '.', 2) ) STORED, action TEXT GENERATED ALWAYS AS ( split_part(split_part(event_id::TEXT, ':', 1), '.', 3) ) STORED, version INTEGER GENERATED ALWAYS AS ( split_part(event_id::TEXT, ':', 2)::INTEGER ) STORED ); -- Indexes for common query patterns CREATE INDEX idx_events_domain ON events(domain); CREATE INDEX idx_events_entity ON events(entity); CREATE INDEX idx_events_action ON events(action); CREATE INDEX idx_events_timestamp ON events(timestamp DESC); CREATE INDEX idx_events_domain_timestamp ON events(domain, timestamp DESC); -- Example queries -- All auth events in last hour SELECT * FROM events WHERE domain = 'auth' AND timestamp > now() - INTERVAL '1 hour'; -- Count by domain SELECT domain, COUNT(*) FROM events GROUP BY domain ORDER BY COUNT(*) DESC;
eventId.ts - TypeScript Implementation
/** * Syntax.ai - Universal Event Model * Make AI-Generated Code Observable. Verifiable. Improvable. */ const EVENT_ID_REGEX = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*:[1-9][0-9]*$/; export interface EventId { domain: string; entity: string; action: string; version: number; } export interface Event<T = Record<string, unknown>> { eventId: EventId; timestamp: Date; payload: T; } export function parseEventId(str: string): EventId { if (!EVENT_ID_REGEX.test(str)) { throw new Error(`Invalid EventID format: ${str}`); } const [path, versionStr] = str.split(':'); const [domain, entity, action] = path.split('.'); return { domain, entity, action, version: parseInt(versionStr, 10), }; } export function formatEventId(id: EventId): string { return `${id.domain}.${id.entity}.${id.action}:${id.version}`; } export function matchesPattern(id: EventId, pattern: string): boolean { const idStr = formatEventId(id); const regexPattern = '^' + pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') .replace(/%/g, '.*') + '$'; return new RegExp(regexPattern).test(idStr); } // Usage example const id = parseEventId('auth.user.login:1'); console.log(id.domain); // 'auth' console.log(matchesPattern(id, 'auth.%')); // true

Machine-Readable Specification

JSON Schema for automated validation and code generation. Use this as the source of truth for building parsers and validators.

eventid-schema.json
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://syntax.ai/event-model/eventid-schema.json", "title": "Syntax.ai Universal Event Model - EventID", "description": "Schema for validating EventID strings and objects", "definitions": { "identifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$", "minLength": 1, "description": "Lowercase identifier starting with a letter" }, "version": { "type": "integer", "minimum": 1, "description": "Positive integer version number" }, "eventIdString": { "type": "string", "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*:[1-9][0-9]*$", "description": "Full EventID string in domain.entity.action:version format" }, "eventIdObject": { "type": "object", "properties": { "domain": { "$ref": "#/definitions/identifier" }, "entity": { "$ref": "#/definitions/identifier" }, "action": { "$ref": "#/definitions/identifier" }, "version": { "$ref": "#/definitions/version" } }, "required": ["domain", "entity", "action", "version"], "additionalProperties": false } }, "oneOf": [ { "$ref": "#/definitions/eventIdString" }, { "$ref": "#/definitions/eventIdObject" } ] }

Start Building with the Event Model

Use the specification to make your AI-generated code observable, verifiable, and improvable.