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