File naming
Each schema lives in its own .json file, named after the event:
vela/schemas/
order.placed.json
order.cancelled.json
payment.failed.json
user.signed-up.json
The filename is just for humans — the eventName field inside the file is what Vela uses to match incoming events. By convention, keep them in sync.
File structure
{
"eventName": "order.placed",
"description": "Emitted when a customer completes checkout",
"fields": [
{
"id": "f1",
"name": "orderId",
"type": "string",
"required": true,
"description": "Opaque order identifier",
"validation": { "min": 1, "max": 128 }
},
{
"id": "f2",
"name": "amountCents",
"type": "number",
"required": true,
"description": "Total order amount in cents",
"validation": { "min": 0 }
},
{
"id": "f3",
"name": "currency",
"type": "enum",
"required": true,
"enumValues": ["USD", "EUR", "GBP", "CAD"]
},
{
"id": "f4",
"name": "customerEmail",
"type": "string",
"required": false,
"validation": { "pattern": "^[^@]+@[^@]+\\.[^@]+$" }
},
{
"id": "f5",
"name": "itemCount",
"type": "number",
"required": false,
"defaultValue": 1
}
],
"metadataFields": [
{
"id": "m1",
"name": "environment",
"type": "string",
"description": "e.g. production, staging"
},
{
"id": "m2",
"name": "traceId",
"type": "string",
"description": "Distributed trace identifier"
}
]
}
Top-level fields
| Field | Required | Description |
|---|
eventName | yes | The event name that must match when ingesting. Dot-separated by convention: order.placed, payment.failed. |
description | no | Human-readable description of what this event represents. Shown in the dashboard. |
fields | yes | Array of field definitions. Minimum one field. |
metadataFields | no | Optional contextual fields (environment, trace ID, version). Not part of the core payload. |
Field properties
| Property | Required | Description |
|---|
id | yes | Unique identifier for this field within the schema. Use a stable value — changing an id doesn’t affect matching, but it’s confusing. |
name | yes | The key name in the event payload (e.g. orderId). Case-sensitive. |
type | yes | Data type — see the table below. |
required | yes | true → event is rejected if this field is missing. false → field is optional. |
description | no | Human-readable description. Shown in the dashboard. |
defaultValue | no | Default value used when the field is absent and required is false. |
enumValues | conditional | Required when type is "enum". Array of allowed string values. |
validation | no | Additional validation constraints (see below). |
Field types
| Type | Description | Validation options |
|---|
string | Text value | min (min length), max (max length), pattern (regex) |
number | Integer or float | min, max |
boolean | true or false | — |
date | ISO-8601 date string (e.g. 2024-06-01T12:00:00Z) | — |
enum | One value from a fixed set of strings | enumValues array required |
object | Nested JSON object | — |
Validation examples
{ "type": "string", "validation": { "min": 1, "max": 64 } }
{ "type": "string", "validation": { "pattern": "^ord_[a-z0-9]+$" } }
{ "type": "number", "validation": { "min": 0, "max": 1000000 } }
{ "type": "enum", "enumValues": ["card_declined", "insufficient_funds", "expired_card"] }
Metadata fields hold optional contextual data that isn’t part of the core event payload — things like deployment environment, trace IDs, or client version.
They have the same id, name, type, and description properties as regular fields, but:
- They are always optional (no
required property)
- They have no
defaultValue or validation
{
"metadataFields": [
{ "id": "m1", "name": "environment", "type": "string" },
{ "id": "m2", "name": "traceId", "type": "string" },
{ "id": "m3", "name": "version", "type": "string" }
]
}
When ingesting, pass metadata in the metadata key:
await ingest.ingest({
event: 'order.placed',
data: { orderId: 'ord_1', amountCents: 4999, currency: 'USD' },
level: 'info',
metadata: { environment: 'production', traceId: 'abc-123' },
});
How diffing works
When you run vela diff or vela push, schemas are matched by eventName. For each schema:
- No remote match → schema will be created
- Remote exists, no changes → skipped
- Remote exists, changes detected → schema will be updated
Fields are compared by name, not id. You can change a field’s id without triggering an update — only changes to name, type, required, enumValues, or validation count as changes.
When updating, the entire fields array replaces the remote version. Removing a field from the local file removes it from the schema. Make sure downstream consumers are updated before removing required fields.