CO2 Intents
Intents are the core command abstraction in the CO2 system. They represent high-level user and system commands that express why an action is being performed, rather than how. In the event sourcing architecture, intents are the entry point that get routed by policy engines to appropriate domain directives.
What Are Intents?
Section titled “What Are Intents?”An Intent is a user-initiated command that captures the intention behind a state change. Rather than directly mutating data, intents are:
- Domain-agnostic: They describe user intention without prescribing implementation details
- Cross-cutting: A single intent can trigger multiple domain aggregates through policies
- Auditable: Every intent is logged, creating a complete audit trail of user actions
- Reversible: Undo/redo operations work at the intent level, not individual events
Intent-Directive Relationship
Section titled “Intent-Directive Relationship”In the CO2 system:
User Action → Intent (user intention) → Policy Router → Directives (implementation) → Domain EventsIntents ask “what should happen?” Directives answer “how should we make it happen?”
Intent Structure and Patterns
Section titled “Intent Structure and Patterns”Base Intent Class
Section titled “Base Intent Class”All intents extend Intent from nomos_core:
abstract class Intent implements IntentBase { late String _id;
@override String get id => _id; // Unique intent ID (UUIDv7)
@override Map<String, dynamic> toJson(); // Serialization
static Intent fromJson(Map<String, dynamic> json); // Deserialization
NomosIntentDescription describe(NomosIntentDescribeCtx ctx); // Human-readable description}Intent Implementation Pattern
Section titled “Intent Implementation Pattern”Every intent follows a consistent pattern:
import 'package:nomos_core/nomos_core.dart';import 'package:contracts_v1/contracts_v1.dart';
class CreateTrackableAssetIntent extends Intent { @override Type get intentType => CreateTrackableAssetIntent;
final TrackableAssetId? assetId; final AssetNickname name; final AssetDescription? description; final AssetType assetType; final UserId createdByUserId; final CustomFieldMap customFields;
CreateTrackableAssetIntent({ this.assetId, required this.name, this.description, required this.assetType, required this.createdByUserId, CustomFieldMap? customFields, }) : customFields = customFields ?? CustomFieldMap.empty();
@override Map<String, dynamic> toJson() => { 'id': id, 'assetId': assetId?.value, 'name': name.value, 'description': description?.value, 'assetType': assetType.code, 'createdByUserId': createdByUserId.value, 'customFields': customFields.toJson(), 'serialisationTargetClassName': 'CreateTrackableAssetIntent', };
static CreateTrackableAssetIntent fromJson(Map<String, dynamic> json) { return CreateTrackableAssetIntent( assetId: json['assetId'] != null ? TrackableAssetId.fromString(json['assetId'] as String) : null, name: AssetNickname.fromString(json['name'] as String?), description: json['description'] != null ? AssetDescription.fromString(json['description'] as String?) : null, assetType: AssetType.fromCode(json['assetType'] as String), createdByUserId: UserId.fromString(json['createdByUserId'] as String), customFields: json['customFields'] != null ? CustomFieldMap.fromJson(json['customFields'] as Map<String, dynamic>) : null, ); }
@override String toString() => 'CreateTrackableAssetIntent($name: $assetType)';
@override NomosIntentDescription describe(NomosIntentDescribeCtx ctx) { final typeLabel = assetType.code != 'generic' ? ' (${assetType.code})' : ''; return NomosIntentDescription( template: '{actor} created asset {assetName}{typeLabel}', variables: { 'actor': actorName(ctx, fallback: createdByUserId.value), 'assetName': name.value, 'typeLabel': typeLabel, }, ); }}Registration Pattern
Section titled “Registration Pattern”All intents must be registered with the Nomos type and factory registries:
// In registration.dartvoid registerIntentsV1() { TypeRegistry.register<CreateTrackableAssetIntent>(); IntentFactoryRegistry.register<CreateTrackableAssetIntent>( (json, id) => CreateTrackableAssetIntent.fromJson(json), ); // ... register all other intents}Human-Readable Descriptions
Section titled “Human-Readable Descriptions”The describe() method generates audit-friendly descriptions using templates:
@overrideNomosIntentDescription describe(NomosIntentDescribeCtx ctx) { return NomosIntentDescription( template: '{actor} granted {user} the {role} role on {resource}', variables: { 'actor': actorName(ctx, fallback: grantedBy.value), 'user': userName(ctx, userId: targetUserId.value, fallback: targetUserId.value), 'role': role.value, 'resource': resourceName(ctx, resourceType: resourceType.name, resourceId: resourceId.value), }, );}The system renders these as: “Alice granted Bob the admin role on Estate XYZ”
Intent Categories by Domain
Section titled “Intent Categories by Domain”The CO2 intents package (intents_v1) organizes intents into logical domains:
Asset Management Intents
Section titled “Asset Management Intents”Intents for creating, updating, and managing trackable assets:
| Intent | Purpose |
|---|---|
CreateTrackableAssetIntent | Create a new asset with properties like name, type, and location |
UpdateTrackableAssetIntent | Modify asset details, structural assignment, or custom fields |
DeleteTrackableAssetIntent | Remove an asset from the system |
BulkUpdateTrackableAssetsIntent | Update multiple assets in a single operation |
MoveTrackableAssetsIntent | Relocate assets to different structural locations |
CreateTrackableAssetCommentIntent | Add a comment/note to an asset |
Example:
final intent = CreateTrackableAssetIntent( name: AssetNickname('HVAC Unit A'), assetType: AssetType.fromCode('hvac'), createdByUserId: userId, structuralAssignment: StructuralAssignment( roomId: RoomId('room-123'), ),);Estate Structure Intents
Section titled “Estate Structure Intents”Intents for managing the physical/logical hierarchy (estates, sites, buildings, rooms):
| Intent | Purpose |
|---|---|
CreateEstateIntent | Create a new estate (top-level organizational unit) |
DeleteEstateIntent | Remove an estate |
CreateSiteWithTopologyDraftIntent | Create a site with an initial topology draft |
CreateBuildingIntent | Add a building to a site |
CreateRoomIntent | Create a room within a building |
PublishSiteTopologyIntent | Finalize and publish topology changes |
SetSiteAnchorIntent | Define geographic anchor point for a site |
SetBuildingFootprintIntent | Define building boundary polygon |
AddFloorPlanTileImageIntent | Attach a floor plan image to a tile |
Example:
final intent = CreateEstateIntent( estateId: EstateId('estate-001'), name: EstateName('Corporate Campus'), address: PostalAddress(...),);Identity and Access Control Intents
Section titled “Identity and Access Control Intents”Intents for managing users, permissions, and organizations:
| Intent | Purpose |
|---|---|
CreateUserIntent | Register a new user account |
CreateOrganizationIntent | Create a new organization |
AddUserToOrganizationIntent | Add user to an organization |
GrantPermissionToUserIntent | Grant specific permissions to a user |
RevokePermissionFromUserIntent | Revoke user permissions |
GrantEstateAccessIntent | Grant user access to an estate |
RevokeEstateAccessIntent | Revoke estate access |
GrantSiteAccessIntent | Grant user access to a site |
RevokeSiteAccessIntent | Revoke site access |
UpdateUserPermissionRoleIntent | Change a user’s role |
Example:
final intent = GrantPermissionToUserIntent( targetUserId: UserId('user-456'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-001', ResourceType.estate), role: Role.estateAdmin, grantedBy: currentUserId, reason: PermissionReasonDescription('Team lead for building maintenance'),);Attachment and Media Intents
Section titled “Attachment and Media Intents”Intents for managing documents, images, and media files:
| Intent | Purpose |
|---|---|
CreateAttachmentIntent | Upload a file (V1 - deprecated, embeds bytes) |
CreateAttachmentIntentV2 | Upload a file (V2 - recommended, uses blob reference) |
DeleteAttachmentIntent | Remove an attachment |
MoveAttachmentBetweenFoldersIntent | Reorganize attachments |
DetachAttachmentIntent | Dissociate attachment from a target |
TranscribeVoiceNoteIntent | Convert voice recording to text (server-only) |
UpdateVoiceNoteTranscriptionIntent | Update transcription results |
CreateFolderIntent | Create an attachment folder |
DeleteFolderIntent | Remove a folder |
Example (V2 - Recommended):
// First: upload blobfinal blobRef = await nomos.blob!.uploadBlob( bytes: pdfBytes, contentType: 'application/pdf',);
// Then: create intent with blob referencefinal intent = CreateAttachmentIntentV2( blobRef: blobRef, fileName: FileName('compliance_report.pdf'), createdBy: userId,);Licensing Intents
Section titled “Licensing Intents”Intents for managing user licenses and seat assignments:
| Intent | Purpose |
|---|---|
CreatePersonalLicenceIntent | Assign a personal license to a user |
CreateOrganizationalLicenceIntent | Create an organizational license |
AssignSeatToUserIntent | Allocate an org license seat to a user |
Example:
final intent = CreatePersonalLicenceIntent( userId: UserId('user-123'), requestedBy: adminUserId, reason: 'New team member onboarding',);Proposal and Responsibility Intents
Section titled “Proposal and Responsibility Intents”Intents for managing change proposals and action items:
| Intent | Purpose |
|---|---|
CreateProposalIntent | Create a detailed proposal for changes |
SubmitProposalIntent | Submit proposal for review |
ApproveProposalIntent | Approve a proposed change |
CreateResponsibilityIntent | Create an action item/responsibility |
UpdateResponsibilityStatusIntent | Track responsibility progress |
CreateResponsibilitiesFromProposalIntent | Auto-create tasks from approved proposal |
Example:
final intent = CreateProposalIntent( proposalId: ProposalId('prop-001'), title: ProposalTitle('HVAC System Upgrade'), description: ProposalDescription('Replace aging HVAC units...'), proposalType: ProposalType('maintenance'), createdBy: userId, estimatedCost: 15000.0, estimatedDuration: ProposalDuration('3 months'), targetImplementationDate: ProposalTargetDate('2024-Q3'),);Catalogue and Asset Creation Intents
Section titled “Catalogue and Asset Creation Intents”Intents for managing asset catalogues and bulk asset creation:
| Intent | Purpose |
|---|---|
CreateCatalogueIntent | Create a new asset catalogue |
CreateCatalogueListingIntent | Add an asset template to catalogue |
CreateAssetFromListingIntent | Create individual asset from listing |
BulkCreateAssetsFromListingIntent | Create multiple assets from same template |
UpdateListingAggregateIntent | Modify a catalogue listing |
DeleteCatalogueListingIntent | Remove listing from catalogue |
Example:
final intent = CreateAssetFromListingWithPositionIntent( listingId: CatalogueListingId('listing-123'), structuralAssignment: StructuralAssignment(roomId: RoomId('room-456')), customNickname: AssetNickname('HVAC Unit B'), createdBy: userId,);Custom Fields Intents
Section titled “Custom Fields Intents”Intents for managing custom properties on layers:
| Intent | Purpose |
|---|---|
DefineCustomFieldIntent | Create a new custom field definition |
AttachCustomFieldToLayerIntent | Add custom field to a site layer |
DetachCustomFieldFromLayerIntent | Remove custom field from a layer |
SetLayerDefaultCustomFieldsIntent | Set default values for a layer |
BulkApplyLayerDefaultsIntent | Apply defaults to multiple assets |
Insights and Analytics Intents
Section titled “Insights and Analytics Intents”Intents for generating reports and insights:
| Intent | Purpose |
|---|---|
TriggerAssetInsightsIntent | Generate asset analytics/KPIs |
CalculateAssetKPIsIntent | Compute key performance indicators |
CreateAssetInsightsDashboardIntent | Create custom dashboard |
GenerateEstateSummaryIntent | Create estate overview report |
CreateDrawingExportIntent | Export drawing/plan as file |
CreateVoiceNoteArchiveIntent | Archive voice notes |
CreateEstateMediaArchiveIntent | Create media backup |
Funding Program Intents
Section titled “Funding Program Intents”Intents for managing retrofit funding and grant programs:
| Intent | Purpose |
|---|---|
CreateFundingProgramIntent | Define a new funding initiative |
ApplyForFundingIntent | Submit funding application |
ProcessFundingApplicationIntent | Approve/reject application |
TrackFundingUtilizationIntent | Monitor spending against budget |
DefineFundingProgramIntent | (Perseus-specific) Define funding rules |
SubmitFundingProgramClaimIntent | Submit claim for reimbursement |
CompleteClaimVerificationIntent | Verify and approve claim |
AwardBenefitIntent | Distribute funding benefit |
Taxonomy Intents
Section titled “Taxonomy Intents”Intents for managing asset type taxonomies:
| Intent | Purpose |
|---|---|
CreateTaxonomyIntent | Create a new taxonomy |
AddTaxonomyCategoryIntent | Add category to taxonomy |
AddTaxonomySubCategoryIntent | Add subcategory |
AddTaxonomyCustomFieldIntent | Define custom field for category |
| (and corresponding update/remove intents) | Modify or delete taxonomy elements |
How Intents Are Dispatched
Section titled “How Intents Are Dispatched”Intent Execution Flow
Section titled “Intent Execution Flow”1. Client Creates Intent ↓2. Intent Serialized (toJson) & Sent ↓3. Server Receives & Deserializes (fromJson) ↓4. Intent Registered in Ledger ↓5. Policy Engine Routes Intent → Directives ↓6. Directives Executed → Domain Events Generated ↓7. Events Applied to Aggregates ↓8. Snapshots UpdatedUsing the Intent Dispatcher
Section titled “Using the Intent Dispatcher”// Create and dispatch an intentfinal intent = CreateTrackableAssetIntent( name: AssetNickname('New Asset'), assetType: AssetType.fromCode('generic'), createdByUserId: currentUserId,);
// Dispatch through Nomosawait nomos.dispatch(intent);// Multiple intents in sequencefinal intents = [ CreateEstateIntent(...), CreateSiteWithTopologyDraftIntent(...), CreateTrackableAssetIntent(...),];
for (final intent in intents) { await nomos.dispatch(intent);}Intent Registry
Section titled “Intent Registry”Intents must be registered before the system starts:
// Initialize the intents modulefinal intentsModule = IntentsV1();intentsModule.registerIntents();
// Now intents can be dispatchedfinal intent = CreateTrackableAssetIntent(...);await nomos.dispatch(intent);Server-Only Intents
Section titled “Server-Only Intents”Some intents require server-side execution (cannot run on client):
// Example: voice transcription (requires OpenAI Whisper)class TranscribeVoiceNoteIntent extends Intent { // ...
@override bool get requiresServerExecution => true;}When dispatched from a client, server-only intents are routed through ServerIntentTransport to a Cloud Run instance.
Key Intents Reference
Section titled “Key Intents Reference”Frequently Used Intents
Section titled “Frequently Used Intents”Asset Creation
Section titled “Asset Creation”CreateTrackableAssetIntent( name: AssetNickname('HVAC Unit'), assetType: AssetType.fromCode('hvac'), createdByUserId: userId, structuralAssignment: StructuralAssignment(roomId: roomId), customFields: CustomFieldMap({'temperature_setpoint': '72'}),)Permission Management
Section titled “Permission Management”GrantPermissionToUserIntent( targetUserId: UserId('user-456'), resourceType: ResourceType.estate, resourceId: ResourceId('estate-001', ResourceType.estate), role: Role.estateAdmin, grantedBy: currentUserId, expiresAt: DateTime.now().add(Duration(days: 365)),)Estate Creation
Section titled “Estate Creation”CreateEstateIntent( estateId: EstateId('estate-001'), name: EstateName('Corporate Campus'), address: PostalAddress( street: 'Main St', city: 'Boston', country: 'US', ),)Site with Initial Topology
Section titled “Site with Initial Topology”CreateSiteWithTopologyDraftIntent( siteId: SiteId('site-001'), siteName: SiteName('Building A'), estateId: EstateId('estate-001'), initialBuilding: CreateBuildingPayload( name: 'Main Structure', levelCount: 3, ),)File Upload (V2 - Recommended)
Section titled “File Upload (V2 - Recommended)”// Step 1: Upload to blob storefinal blobRef = await nomos.blob!.uploadBlob( bytes: fileBytes, contentType: 'application/pdf',);
// Step 2: Create intent with referenceCreateAttachmentIntentV2( blobRef: blobRef, fileName: FileName('document.pdf'), createdBy: userId,)Best Practices
Section titled “Best Practices”1. Use Strong Types
Section titled “1. Use Strong Types”Always use domain types (not String):
// Goodfinal intent = CreateTrackableAssetIntent( name: AssetNickname('Unit A'), createdByUserId: UserId('user-123'),);
// Bad - avoids type safetyfinal intent = CreateTrackableAssetIntent( name: AssetNickname('Unit A'), createdByUserId: UserId('user-123'), // Use UserId, not String);2. Prefer V2 Attachments
Section titled “2. Prefer V2 Attachments”Always use CreateAttachmentIntentV2 for new code:
// Good - V2 with blob referencefinal intent = CreateAttachmentIntentV2( blobRef: blobRef, fileName: FileName('doc.pdf'), createdBy: userId,);
// Avoid - V1 embeds bytesfinal intent = CreateAttachmentIntent( fileBytes: largeFileData, fileName: FileName('doc.pdf'), createdBy: userId,);3. Always Provide Actor Context
Section titled “3. Always Provide Actor Context”Intents track who initiated the action:
CreateTrackableAssetIntent( name: AssetNickname('Asset'), assetType: AssetType.fromCode('generic'), createdByUserId: currentUserId, // Always set this)4. Use Descriptive Titles
Section titled “4. Use Descriptive Titles”When creating proposals or responsibilities, use clear titles:
// Goodtitle: ProposalTitle('Replace HVAC Unit A - Expected Downtime 2hrs'),
// Avoid vague titlestitle: ProposalTitle('HVAC change'),5. Bulk Operations for Performance
Section titled “5. Bulk Operations for Performance”For large datasets, use bulk intents:
// Good - single intentBulkUpdateTrackableAssetsIntent( assetIds: [asset1, asset2, asset3], updates: {...},)
// Avoid - creates overheadfor (final assetId in assetIds) { await nomos.dispatch(UpdateTrackableAssetIntent(...));}6. Validate Before Dispatch
Section titled “6. Validate Before Dispatch”Intents are serialized and sent, so validate early:
if (name.value.isEmpty) { throw ArgumentError('Asset name cannot be empty');}
if (structuralAssignment != null && roomId == null) { throw ArgumentError('Room ID required for structural assignment');}
final intent = CreateTrackableAssetIntent(...);await nomos.dispatch(intent);Intent vs Directive vs Event
Section titled “Intent vs Directive vs Event”Understanding the three levels:
| Aspect | Intent | Directive | Event |
|---|---|---|---|
| Level | User-facing command | Implementation instruction | Immutable fact |
| Source | User action or system trigger | Policy engine | Command execution |
| Purpose | Express user intention | Specify how to implement intent | Record what happened |
| Mutability | Can be rolled back | Determines logic | Immutable history |
| Example | CreateTrackableAssetIntent | ProvisionAssetDirective | AssetCreatedEvent |
Flow:
Intent (user says "create asset") ↓ (policy routes)Directives (says "provision asset, assign to room, update inventory") ↓ (execution)Events (fact: "asset created", "asset assigned", "inventory updated")Architectural Notes
Section titled “Architectural Notes”Intent Serialization
Section titled “Intent Serialization”All intents must be JSON-serializable for storage and network transmission. The serialisationTargetClassName field enables proper deserialization:
@overrideMap<String, dynamic> toJson() => { 'id': id, 'name': name.value, 'serialisationTargetClassName': 'CreateTrackableAssetIntent', // Required};Intent Immutability
Section titled “Intent Immutability”Once created and dispatched, intents cannot be modified. They represent a historical record of user intent. Updates require new intents (e.g., UpdateTrackableAssetIntent).
Cross-Aggregate Consistency
Section titled “Cross-Aggregate Consistency”A single intent can affect multiple aggregates through policy routing. The policy engine ensures consistency:
CreateSiteWithTopologyDraftIntent → Creates: SiteRootAggregate + TopologyDraftAggregate + multiple BuildingAggregates → Maintains: referential integrity across aggregatesTroubleshooting
Section titled “Troubleshooting”Intent Not Dispatching
Section titled “Intent Not Dispatching”Problem: Intent dispatch fails silently
await nomos.dispatch(intent); // No error, but no effectSolution: Ensure intent is registered:
TypeRegistry.register<YourIntent>();IntentFactoryRegistry.register<YourIntent>( (json, id) => YourIntent.fromJson(json),);Deserialization Errors
Section titled “Deserialization Errors”Problem: ArgumentError: JSON must contain a "serialisationTargetClassName" field
Solution: Verify toJson() includes the serialization field:
@overrideMap<String, dynamic> toJson() => { 'id': id, 'serialisationTargetClassName': 'YourIntent', // Add this // ... other fields};Server-Only Intent on Client
Section titled “Server-Only Intent on Client”Problem: TranscribeVoiceNoteIntent called from Flutter client throws error
Solution: Server-only intents must be routed through server transport:
// Make sure ServerIntentTransport is configuredfinal nomos = NomosConfig() .withServerIntentTransport(...) .build();
await nomos.dispatch(TranscribeVoiceNoteIntent(...)); // Routes to serverReferences
Section titled “References”- Package:
intents_v1at/dart_packages/co2/intents/intents_v1/ - Base Class:
Intentinnomos_core - Registry:
registration.dart- All intent registrations - Utilities:
describe_utils.dart- Intent description helpers