Events & SSE

Every mutation fires an event. Every event tells a story.

Structured lifecycle events power webhooks, prompt hooks, audit logging, and real-time streaming across the entire platform.

Architecture

Every mutating API request

Event recorded
Structured event row written in the same transaction as the write
Streamed over pub/sub
Event fans out to SSE, webhook, and prompt-hook consumers
Audit entry written
Immutable audit record captured alongside the event
Redis pub/sub

SSE endpoint

/api/events/stream

Real-time streaming to browser clients

Webhook dispatcher

webhook endpoints → HTTP

POST deliveries to external servers

Prompt dispatcher

prompt hooks → LLM / MCP

AI execution via configured templates

When a user creates a task, updates an employee record, or completes a workflow step, the platform records the event and an audit entry atomically with the write, then fans it out to real-time consumers — SSE, webhooks, and prompt hooks — on the same tick.

Append-only

Events are append-only. Once written, they are never modified.

Lifecycle phases

Every entity in the system fires events at standardized lifecycle points:

PhaseFires when
pre_createBefore the INSERT -- allows pre-validation hooks
post_createAfter a successful INSERT
pre_updateBefore the UPDATE
post_updateAfter a successful UPDATE
pre_archiveBefore a soft-delete or deactivation
post_archiveAfter a successful soft-delete
pre_exportBefore an export operation begins
post_exportAfter export data is assembled

The pre_* phases fire before the database write and are used by prompt hooks for validation and review. The post_* phases fire after the write commits and are used by webhooks and downstream integrations.

Event type naming

Event types follow the pattern {aggregate_type}.{phase}:

  • task.post_create
  • employee.post_update
  • workflow.post_archive
  • survey.post_create

Some domain-specific events use descriptive names instead of lifecycle phases:

  • workflow.started, workflow.completed, workflow.cancelled
  • step.passed, step.failed, step.approved
  • auth.login, auth.logout, auth.password_changed

Aggregate types

Events cover every mutable entity in the platform, including: task, employee, workflow, step, survey, survey_response, feedback, kudos, calendar_event, announcement, position, role, team, user, organization, api_key, webhook_endpoint, and more.

EventContext payload

Every event carries a standardized EventContext in its payload, giving downstream consumers enough information to act without re-querying the API:

interface EventContext {
  org_id: number;
  actor_id: number | null;
  workflow_id?: number;
  workflow_name?: string;
  step_id?: number;
  step_name?: string;
  record_id: number;
  record_type: string;
  record_name?: string;
  action: string;
  timestamp: string;
}
FieldDescription
org_idThe organization where the event occurred
actor_idThe user who performed the action (null for system events)
workflow_idThe workflow context, if the action happened inside a workflow
step_idThe workflow step context, if applicable
record_idThe primary key of the affected entity
record_typeThe aggregate type (e.g. task, employee)
record_nameA human-readable label for the record
actionThe lifecycle phase or domain-specific action
timestampISO 8601 timestamp of when the event occurred

REST API

List events

Retrieve events with pagination and filters.

GET /api/events
ParameterTypeDescription
aggregate_typestringFilter by entity type (e.g. task)
event_typestringFilter by full event type (e.g. task.post_create)
fromISO 8601Events after this timestamp
toISO 8601Events before this timestamp
limitintegerResults per page (1--100, default 25)
pageintegerPage number (default 1)
curl "https://api.wrk.ing/api/events?aggregate_type=task&limit=10" \
  -H "Authorization: Bearer $TOKEN"
const res = await fetch(
  "https://api.wrk.ing/api/events?aggregate_type=task&limit=10",
  { headers: { Authorization: `Bearer ${token}` } }
);
const { data, total } = await res.json();
import requests

res = requests.get(
    "https://api.wrk.ing/api/events",
    params={"aggregate_type": "task", "limit": 10},
    headers={"Authorization": f"Bearer {token}"},
)
data = res.json()
{
  "data": [
    {
      "id": 8401,
      "org_id": 1,
      "aggregate_type": "task",
      "aggregate_id": 234,
      "event_type": "task.post_create",
      "payload": { "..." },
      "occurred_at": "2026-04-12T14:30:00.000Z"
    }
  ],
  "total": 142
}

Entity event history

Get the full event history for a specific entity, ordered by sequence number.

GET /api/events/:aggregateType/:aggregateId
curl https://api.wrk.ing/api/events/task/234 \
  -H "Authorization: Bearer $TOKEN"
const res = await fetch(
  "https://api.wrk.ing/api/events/task/234",
  { headers: { Authorization: `Bearer ${token}` } }
);
const history = await res.json();
res = requests.get(
    "https://api.wrk.ing/api/events/task/234",
    headers={"Authorization": f"Bearer {token}"},
)
history = res.json()

Server-Sent Events

The SSE endpoint opens a persistent connection and streams events as they happen. This is useful for building real-time dashboards, live activity feeds, or monitoring tools.

GET /api/events/stream

Connecting

const token = "your-access-token";
const source = new EventSource(
  `https://api.wrk.ing/api/events/stream?token=${token}`
);

source.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log(`${data.event_type}: ${data.aggregate_type}#${data.aggregate_id}`);
};

source.onerror = () => {
  console.log("Connection lost, reconnecting...");
};
import sseclient, requests

url = "https://api.wrk.ing/api/events/stream"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers, stream=True)

for event in sseclient.SSEClient(response).events():
    print(event.data)
curl -N https://api.wrk.ing/api/events/stream \
  -H "Authorization: Bearer $TOKEN"

Connection behavior

  • The server sends a heartbeat comment every 30 seconds to keep the connection alive through proxies and load balancers.
  • If the connection drops, EventSource automatically reconnects with exponential backoff.
  • Each event is published as a data: line containing the full JSON event payload.
  • Events are scoped to your organization. You only receive events for the org associated with your token.

Output appears as a continuous stream:

: heartbeat

data: {"id":8401,"event_type":"task.post_create","aggregate_type":"task",...}

data: {"id":8402,"event_type":"employee.post_update","aggregate_type":"employee",...}

: heartbeat

Event-driven automation

Events are the trigger for two powerful automation systems:

Webhooks

Register HTTP endpoints to receive events as POST requests. Filter by event type to only receive what you need. See the Webhooks guide.

Prompt hooks

Attach AI prompt templates to lifecycle events. When a matching event fires, wrk!ng renders the template with the event context and calls an LLM. See the AI integration guide.

Both systems consume events from the same Redis pub/sub channel, ensuring consistent and immediate delivery.