Event Schemas#
Register your custom event types and attach JSON Schemas so every producer writes the same payload shape into the shared event ledger.
Why register schemas?#
Events in Outeract share one ledger: messages, system events, and your custom events all land in the same stream and flow out through the same webhook subscriptions. A payload contract keeps that stream trustworthy:
- Producers can’t drift. An order service that forgets
tracking_numbergets an error, not a silent bad row. - Consumers can rely on shape. Webhook handlers and dashboards read
payload.order_idwithout defensive checks. - Types are self-documenting. The schema shows up in the console next to the event type it governs.
Schemas are optional; an event type without one accepts any valid JSON.
The EventSchema record#
Each event type is an EventSchema record:
| Field | Description |
|---|---|
id | UUID; events reference their type by ID, so renames are instant |
name | Type name, e.g. order.shipped (lowercase letters, digits, dots, underscores) |
jsonSchema | Optional JSON Schema (Draft-07) validated against payload |
enforceValidation | true (default): invalid payloads are rejected; false: warn but accept |
appId | Set for your app’s types; null for built-in global types (message.*, etc.) |
Creating an event with an unregistered type auto-creates the type for your app, with no schema attached. Registering explicitly is how you add the contract.
Reserved prefixes#
Built-in system types use reserved prefixes that your custom types can’t start with: file.*, link_code.*, message.*, user.*
Anything else is yours: order.shipped, payment.stripe.succeeded, analytics.page_view.
Register a type with a schema#
Event schemas are managed in the console, under each application’s Event Types section. Create a type named order.shipped, turn on enforced validation, and give it this JSON Schema:
{
"type": "object",
"required": ["order_id", "tracking_number"],
"properties": {
"order_id": { "type": "string", "pattern": "^order_[a-zA-Z0-9]+$" },
"tracking_number": { "type": "string" },
"carrier": { "type": "string", "enum": ["ups", "fedex", "dhl", "royal_mail"] },
"shipped_at": { "type": "string", "format": "date-time" }
}
}To register a type as a plain identifier (no validation yet), create it without a schema.
Inspect registered types#
The Event Types list shows every type available to the application (the ones you registered plus the global built-ins) with each type’s schema, enforcement setting, creation date and per-type stats. Names resolve against your app first, then the global built-ins.
What happens on validation failure#
When you create an event, Outeract resolves its type and validates the payload:
- No registered type → the type is auto-created and the event is accepted (any JSON).
- Type registered, no
jsonSchema→ accepted. - Schema exists,
enforceValidation: true→ invalid payloads are rejected: the mutation returns a GraphQL error and no event is stored. - Schema exists,
enforceValidation: false→ validation errors are logged, but the event is stored anyway (warn-but-accept).
A rejected createEvent fails like this:
mutation {
createEvent(
userId: "USER_ID"
eventType: "order.shipped"
payload: { order_id: "order_12345" } # missing tracking_number
) {
id
}
}{
"errors": [
{
"message": "Invalid payload for order.shipped: tracking_number: 'tracking_number' is a required property"
}
]
}Validation applies at event creation. Changing a schema later does not re-validate or reject events already in the ledger.
Evolving a schema#
A type’s detail page has a schema builder for editing the JSON Schema, toggling enforcement, renaming and deleting. To add a new optional field, extend the properties:
{
"type": "object",
"required": ["order_id", "tracking_number"],
"properties": {
"order_id": { "type": "string" },
"tracking_number": { "type": "string" },
"carrier": { "type": "string" },
"shipped_at": { "type": "string", "format": "date-time" },
"estimated_delivery": { "type": "string", "format": "date" }
}
}Clearing the schema leaves the type in place as a plain identifier. Renaming is instant, because events reference their type by ID rather than by name.
Guidance#
- Add optional fields freely. New optional properties never break existing producers.
- Tighten in two steps. To add a required field, ship the producer change first, then update the schema, or turn enforcement off during the migration and watch the logs before re-enabling.
- Rename types, don’t fork them. Renaming keeps history intact; creating a parallel type splits your ledger. Remember to update webhook subscription patterns that matched the old name.
- Don’t loosen contracts silently. Consumers depend on the schema, so treat removals of required fields as breaking changes for your webhook handlers.
See Also#
- Custom Events - creating events, edges, and event chains
- Events - how the event ledger works
- Outbound Webhooks - subscribing to your custom types
- Event Types Reference - built-in event types