Standards-First Messaging — FabrCore Adopts CloudEvents
FabrCore has two messaging primitives: AgentMessage for request/response communication and EventMessage for fire-and-forget event delivery. With this release, EventMessage adopts a CloudEvents-compatible structure — bringing a CNCF-backed standard into the agent runtime so events are self-describing, interoperable, and ready for integration with external systems.
Why Standards Matter for Agent Events
When agents communicate through events, the event format is a contract between producer and consumer. A custom format works fine inside a single codebase, but it becomes a burden the moment events need to cross system boundaries — flowing into a message broker, triggering a webhook, or being consumed by a partner's integration. Every custom format requires a custom adapter.
CloudEvents is a specification from the Cloud Native Computing Foundation (CNCF) that defines a common envelope for event data. It specifies a small set of required attributes — id, source, type, and time — plus optional attributes like subject, data, and datacontenttype. By aligning EventMessage with this specification, FabrCore events can be serialized into standard CloudEvents JSON and consumed by any system that understands the spec.
The EventMessage Structure
Here is the full EventMessage class. Each property maps directly to a CloudEvents attribute:
public class EventMessage
{
// CloudEvents required attributes
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Type { get; set; } // e.g. "order.created"
public string Source { get; set; } // Producer handle
public DateTimeOffset Time { get; set; } = DateTimeOffset.UtcNow;
// CloudEvents optional attributes
public string? Subject { get; set; }
public string? Data { get; set; }
public string? DataContentType { get; set; }
public byte[]? BinaryData { get; set; }
// Routing (FabrCore-specific)
public string Namespace { get; set; } // Stream namespace
public string Channel { get; set; } // Channel within namespace
// Extensions
public Dictionary<string, string>? Args { get; set; }
public string? TraceId { get; set; } = Guid.NewGuid().ToString();
}
| Property | CloudEvents Attribute | Purpose |
|---|---|---|
Id | id | Unique event identifier (auto-generated GUID) |
Source | source | The agent handle that produced the event |
Type | type | Event type using dot notation (e.g., "order.created") |
Time | time | When the event occurred (defaults to UTC now) |
Subject | subject | Optional subject or entity the event relates to |
Data | data | String payload (JSON, XML, plain text) |
DataContentType | datacontenttype | MIME type of the data (e.g., "application/json") |
BinaryData | data (binary) | Binary payload for non-text data |
The Namespace, Channel, Args, and TraceId properties are FabrCore-specific extensions. Namespace and Channel control Orleans stream routing, while TraceId integrates with OpenTelemetry distributed tracing.
Sending and Receiving Events
Events are sent using SendEvent on IFabrCoreAgentHost (from within an agent) or via the REST API at POST /fabrcoreapi/agent/event/{handle}. Events are fire-and-forget — no response is returned.
var eventMsg = new EventMessage
{
Type = "order.status-changed",
Source = fabrcoreAgentHost.GetHandle(),
Subject = "ORD-12345",
Channel = "fulfillment-agent",
Data = "{\"orderId\": \"ORD-12345\", \"status\": \"shipped\"}",
DataContentType = "application/json"
};
await fabrcoreAgentHost.SendEvent(eventMsg);
On the receiving side, events arrive in the OnEvent lifecycle method. The agent can inspect Type to route to the correct handler:
public override Task OnEvent(EventMessage eventMessage)
{
switch (eventMessage.Type)
{
case "order.status-changed":
logger.LogInformation(
"Order {Subject} status changed at {Time}",
eventMessage.Subject,
eventMessage.Time);
break;
case "inventory.low-stock":
// Trigger replenishment workflow
break;
}
return Task.CompletedTask;
}
FabrCore distinguishes between AgentMessage and EventMessage at the transport level. AgentMessage supports Request, OneWay, and Response kinds for conversational communication. EventMessage is strictly one-way — it flows through the Orleans streaming infrastructure and is delivered to the agent's OnEvent handler, never to OnMessage.
Interoperability and What Comes Next
Because EventMessage maps cleanly to CloudEvents attributes, serializing it to the CloudEvents JSON format is straightforward. This opens up several integration patterns:
- Webhook delivery — Serialize events as CloudEvents JSON and POST them to external webhook endpoints. The receiver does not need to know anything about FabrCore.
- Message broker bridging — Forward events to Azure Event Grid, Kafka, or RabbitMQ using CloudEvents-compatible bindings.
- Cross-system choreography — Multiple systems producing and consuming CloudEvents can participate in the same event-driven workflow without custom adapters.
- Observability — The
TraceIdproperty integrates with OpenTelemetry, allowing event flows to be traced end-to-end across agent boundaries.
The Args dictionary on EventMessage serves as the CloudEvents extension attributes mechanism — a place for domain-specific metadata that does not belong in the core envelope. For example, a priority level, a tenant identifier, or a correlation key can all ride along as extension attributes without modifying the base event structure.
Adopting a standard does not mean giving up flexibility. The Namespace and Channel properties provide FabrCore-specific routing that does not exist in the CloudEvents spec, while the core attributes ensure that any event can be understood outside the FabrCore runtime.
Built with FabrCore on .NET 10.
Builder of FabrCore and OpenCaddis.