Skip to content

Add an extension attribute

Mechanically

Extensions are a map[string]string on the event, keyed without the ce- prefix:

e := cloudevents.Event{
    ID:     "01JQ8Z3F7K2M4N6P8R0T2V4W6X",
    Source: "/phpbotscout/ingest",
    Type:   "uk.phpboyscout.orders.created",
    Extensions: map[string]string{
        "scope": "urn:phpboyscout:scope:continuity:01JQ8Z3F7K2M4N6P8R0T2V4W6X",
    },
}

That is written as ce-scope on the wire and read back under scope.

The name has to satisfy the grammar

Check it with ValidExtensionName if you are generating names; Marshal refuses an invalid one either way.

Rule Enforced Why
One or more characters Yes MUST
Lower-case ASCII letters and digits only Yes MUST — no hyphens, no underscores, no dots
Not the name data Yes MUST — some event formats reserve it
Not one of the eight attribute names Yes Extensions are "additional context attributes with distinct names"
Starts with a letter No SHOULD
Twenty characters or fewer No SHOULD

The fourth rule is the one with teeth. id, source, specversion, type, datacontenttype, dataschema, subject and time are refused with ErrReservedExtensionName — without it, Extensions["id"] would overwrite ce-id on the way out and the event would marshal with a hijacked identity and no error at all.

An extension value must also be non-empty and free of control characters. An empty value writes no header, so it would vanish in transit; a control character would be rewritten by net/http rather than refused.

The two SHOULDs are deliberately not enforced. Refusing a twenty-one character name would reject events the rest of the world accepts, which is the interoperability failure that implementing a standard rather than inventing one exists to avoid.

The constraint is narrow because some protocols treat metadata as case-sensitive and others do not, and one event may cross several of them in a single delivery.

The bar for adding one at all

Every attribute must be safe to log

Attributes ride in headers, headers are logged by default, and nobody audits a header the way they audit a payload field. This estate routes free-form strings bound for telemetry through go/redact; attributes bypass that entirely.

  • Opaque identifiers only. Never a display name, a channel name, a guild name, or anything a person chose.
  • Nothing a redaction pass would catch, because nothing routes attributes through one.
  • A payload has a schema and a review. An attribute has neither, so the bar is higher.

If the value is not an opaque identifier, it belongs in the payload.

The one that already exists

scope is the estate's tenancy attribute, and it is ADVISORY, AND NEVER THE AUTHORITY. It is a claim in a message, for routing, filtering and legibility. Enforcement is NATS accounts with subject-scoped credentials, and a consumer that filters on scope has isolated nothing.