Standards-First Messaging — FabrCore Adopts CloudEvents

Eric Brasher March 5, 2026 at 11:20 AM 5 min read

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:

C# — EventMessage Class
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();
}
PropertyCloudEvents AttributePurpose
IdidUnique event identifier (auto-generated GUID)
SourcesourceThe agent handle that produced the event
TypetypeEvent type using dot notation (e.g., "order.created")
TimetimeWhen the event occurred (defaults to UTC now)
SubjectsubjectOptional subject or entity the event relates to
DatadataString payload (JSON, XML, plain text)
DataContentTypedatacontenttypeMIME type of the data (e.g., "application/json")
BinaryDatadata (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.

C# — Sending an Event from an Agent
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:

C# — Handling Events in OnEvent
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 TraceId property 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.


Eric Brasher

Builder of FabrCore and OpenCaddis.