Events#
Events are the core data model in Outeract. Every message, status update, and action is represented as an immutable event, providing a complete audit trail of all activity.
Overview#
Outeract uses an event-sourced architecture where:
- All state changes are captured as events
- Events are immutable (never modified after creation)
- Current state is derived from the event stream
- Full history is always available
flowchart LR
subgraph Stream["Event Stream"]
E1["message.inbound<br/>t=0"] --> E2["message.outbound<br/>t=1"]
E2 --> E3["message.delivered<br/>t=2"]
E3 --> E4["message.read<br/>t=3"]
E4 --> More["..."]
endEvent Model#
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
app_id | UUID | Application this event belongs to |
event_type_id | UUID | Reference to EventSchema |
status | string | pending, processing, completed, failed |
payload | JSON | Event-specific data |
origin_event_id | UUID | Parent event (for status updates) |
processed_at | datetime | When the event was processed |
created_at | datetime | When the event was created |
Event Types#
Events are categorized by type. Built-in types follow a hierarchical naming convention:
Message Events#
message.inbound - Incoming message from a user
message.outbound - Outgoing message to a userPayload Structure:
{
"type": "message",
"message": {
"text": "Hello, world!",
"role": "user"
},
"platform": "whatsapp",
"external_message_id": "wamid.xxx",
"created_at": "2024-01-15T10:30:00Z",
"sent_at": "2024-01-15T10:30:01Z",
"delivered_at": "2024-01-15T10:30:02Z",
"read_at": null,
"failed_at": null
}Link Code Events#
link_code.generated - A link code was created
link_code.activation - A link code was usedGeneration Payload:
{
"code": "1234-5678-9012-3456",
"expiry_minutes": 15,
"max_uses": 1,
"uses": 0,
"generated_by_identity_id": "pu_abc123"
}User Events#
user.merged - Two user records were mergedCustom Events#
You can define custom event types using any valid name. Event type names must:
- Use lowercase letters, numbers, dots, and underscores only
- Match the pattern:
^[a-z0-9_.]+$ - Not use reserved prefixes:
message.*,user.*,system.*,link_code.*
order.created
payment.completed
support.ticket.opened
purchase
subscription.renewedEvent Relationships#
Events are connected to other entities via edges:
flowchart TB
Event["Event<br/>(message.inbound)"]
Event --> SB["sent_by edge"]
Event --> ST["sent_to edge"]
Event --> AT["attachment edge"]
SB --> PU["Platform User"]
ST --> SU["System User"]
AT --> F["File"]Querying Events#
Get Recent Events#
query {
events(limit: 20) {
id
eventType
payload
createdAt
}
}Filter by Event Type#
query {
events(eventTypes: ["message.inbound", "message.outbound"], limit: 50) {
id
eventType
payload
}
}Filter by Date Range#
query {
events(
since: "2024-01-01T00:00:00Z"
until: "2024-01-31T23:59:59Z"
) {
id
eventType
createdAt
}
}Filter by Conversation#
Use conversationId to get all events in a specific conversation:
query {
events(conversationId: "conv_abc123") {
id
eventType
payload
}
}Filter by Relationships (RelationFilter)#
Use relatedTo for advanced filtering based on event-to-event relationships. Since conversations are events, this is the primary way to filter by conversation. Supports nested AND/OR conditions:
Simple filter - events in a conversation:
query {
events(relatedTo: {eventIds: ["conv_123"], direction: "outgoing"}) {
id
eventType
}
}OR filter - events in ANY of the conversations:
query {
events(
relatedTo: {
or_: [
{eventIds: ["conv_1"], direction: "outgoing"},
{eventIds: ["conv_2"], direction: "outgoing"}
]
}
) {
id
eventType
}
}AND filter - events in ALL conversations (intersection):
query {
events(
relatedTo: {
and_: [
{eventIds: ["conv_1"], direction: "outgoing"},
{eventIds: ["conv_2"], direction: "outgoing"}
]
}
) {
id
eventType
}
}Nested conditions - (A OR B) AND C:
query {
events(
relatedTo: {
and_: [
{
or_: [
{eventIds: ["conv_1"], direction: "outgoing"},
{eventIds: ["conv_2"], direction: "outgoing"}
]
},
{eventIds: ["conv_3"], direction: "outgoing"}
]
}
) {
id
eventType
}
}RelationFilter Input:
| Field | Type | Description |
|---|---|---|
eventIds | [UUID] | Event IDs to filter by (e.g., conversation IDs) |
direction | String | Edge direction: "outgoing", "incoming", or "any" (default) |
and_ | [RelationFilter] | Nested filters combined with AND |
or_ | [RelationFilter] | Nested filters combined with OR |
Note: Specify only one of
eventIds,and_, oror_per filter object.Note:
RelationFilteronly matches event-to-event relationships. For filtering by user, use theuserIdparameter instead.
Filter by Edge Target (Arbitrary Nodes)#
Use relatedNodeId to filter events by an edge (event as source) pointing at an arbitrary node, not only other events. For example, all events with any edge targeting a specific user:
query {
events(relatedNodeId: "user_xyz789") {
id
eventType
}
}Two optional parameters narrow the match (both are ignored unless relatedNodeId is given):
| Parameter | Type | Description |
|---|---|---|
relatedNodeId | UUID | Only events having an edge whose target node ID equals this |
relatedNodeType | String | Require the edge’s target node type (e.g. "user"). Omit for any type |
relatedEdgeType | String | Require the edge’s edge type (e.g. "sent_to", "participant"). Omit for any edge type |
This combines with all other filters (AND) and is applied at the database level, so cursor pagination stays exact: last: 50 returns 50 matching events. A typical use is scoping a user’s history to one counterpart:
query {
# The 50 newest events for a user that also have a sent_to edge to another node
events(
userId: "user_abc123"
relatedNodeId: "system_user_xyz789"
relatedEdgeType: "sent_to"
last: 50
) {
id
eventType
payload
}
}Get Events for a User#
query {
events(userId: "user_abc123", limit: 50) {
id
eventType
payload
}
}Combining Filters#
All filters can be combined and are applied with AND logic:
query {
# Events for a user in a specific conversation
events(userId: "user_abc123", conversationId: "conv_xyz789") {
id
eventType
payload
}
}Creating Events#
Via Message API#
When you send a message, an event is automatically created:
mutation {
sendMessage(
platformConnectionId: "pc_abc123"
recipientUserId: "user-uuid"
message: "Hello!"
) {
id
eventTypeName
}
}This creates a message.outbound event with appropriate edges.
Custom Events#
Create custom events for your application:
mutation {
createEvent(
eventType: "order.created"
payload: {
order_id: "order_12345"
total: 99.99
currency: "USD"
}
) {
id
eventType
payload
}
}Event Status#
Events have a lifecycle status:
| Status | Description |
|---|---|
pending | Event created, not yet processed |
processing | Currently being processed |
completed | Successfully processed |
failed | Processing failed |
Status Updates#
Status updates create child events linked via origin_event_id:
flowchart TB
Parent["message.outbound<br/>id: evt_001<br/>status: completed"]
Parent -->|origin_event_id| Delivered["delivered status<br/>evt_002"]
Parent -->|origin_event_id| Read["read status<br/>evt_003"]
Parent -->|origin_event_id| Failed["failed status"]Query status updates:
query {
event(id: "evt_001") {
id
eventType
childEvents {
id
eventType
payload
}
}
}Event Schemas#
Event types are defined by EventSchemas, managed in the console under Settings → Event Schemas. Each has a name, an optional JSON Schema, an enforce-validation flag, and a global/app-specific scope.
Built-in (Global) Schemas#
Global schemas are available to all applications:
message.inboundmessage.outboundlink_code.generatedlink_code.activation
Custom Schemas#
Define custom event types with JSON Schema validation. Add a schema named order.created with enforced validation and this JSON Schema:
{
"type": "object",
"required": ["order_id", "total"],
"properties": {
"order_id": { "type": "string" },
"total": { "type": "number", "minimum": 0 },
"currency": { "type": "string", "enum": ["USD", "EUR", "GBP"] }
}
}Event Subscriptions (Webhooks)#
Subscribe to events via outbound webhooks. Create a subscription in the console under Settings → Webhooks, pointing at your endpoint (e.g. https://myapp.com/webhooks/outeract) and matching the event types you care about (e.g. message.inbound, order.*).
When matching events occur, Outeract POSTs to your URL:
{
"event_id": "evt_abc123",
"event_type": "message.inbound",
"app_id": "app_xyz789",
"payload": {
"message": { "text": "Hello!" }
},
"edges": {
"sent_by": {
"identity_id": "pu_123",
"external_id": "+14155551234"
}
},
"created_at": "2024-01-15T10:30:00Z"
}Best Practices#
1. Use Event Types Consistently#
Follow the category.action naming convention for custom events.
2. Include Relevant Data in Payloads#
Store enough context in the payload to understand the event without external lookups.
3. Don’t Store Sensitive Data#
Avoid putting passwords, tokens, or PII in event payloads.
4. Use Edges for Relationships#
Link events to users and files via edges, not payload fields.
5. Handle Idempotency#
Events may be delivered multiple times. Use external_message_id for deduplication.
Related Concepts#
- Edges - Relationships between events and entities
- Users - How users relate to events
- Event Types Reference - Complete list of built-in types