Event Protocol
1. Purpose
Section titled “1. Purpose”The event protocol is the stable wire contract between instrumented applications and Wayscribe.
It must remain separate from:
- database models
- internal API implementation
- UI view models
- provider-specific telemetry formats
2. Envelope
Section titled “2. Envelope”interface EventEnvelopeV01 { protocolVersion: "0.1"; event: JourneyEventV01;}Every request event is wrapped in an envelope so future protocol versions can be handled explicitly.
3. Journey event
Section titled “3. Journey event”type JourneyOperation = | "received" | "identified" | "transformed" | "validated" | "persisted" | "published" | "consumed" | "delivered" | "failed" | "retried" | "completed";
interface JourneyEventV01 { id: string; journeyId: string;
environment: string; service: string;
entity: { type: string; id: string; };
operation: JourneyOperation; name: string; timestamp: string;
aliases?: Record<string, string>; displayableAliases?: string[]; // alias types a reader may see in full journeyLabel?: string; // public display text for the journey, 1 to 200 characters
durationMs?: number; // whole milliseconds, 0 to 2147483647 parentEventId?: string;
traceId?: string; spanId?: string; messageId?: string; correlationId?: string;
input?: unknown; output?: unknown;
error?: { type?: string; message: string; code?: string; stack?: string; };
runtime?: { language?: string; // at most 64 version?: string; // at most 64 hostname?: string; // at most 256 processId?: number; sdk?: { name: string; // 1 to 128 characters version: string; // 1 to 64 characters commit?: string; // 1 to 128 characters }; };
deployment?: { gitCommit?: string; version?: string; image?: string; };
metadata?: Record<string, unknown>;}journeyLabel is optional display text for the event’s journey, written by the
instrumenting code, for example "Mirantis · Senior SWE, AI Infra". It holds 1
to 200 characters, counted as Unicode code points like every other string
maximum in this schema. An empty string is refused as invalid_event with the
detail path event.journeyLabel, rather than read as clearing the label: a
host that wants no label sends none, and an event without the field leaves the
journey’s label as it was. When events carry different labels, the label of the
event with the latest timestamp wins, whatever order the events arrive in.
Timestamps are compared at millisecond precision, the precision they are
stored at, so two events less than a millisecond apart tie. A tie is broken by
the order the server received the events, the order the journey’s timeline
shows them in, and a tie on both by the larger event id, compared byte by
byte. Events a client sends one after another, in one batch or in successive
requests, are received in that order; events sent concurrently in different
requests are received in no guaranteed order. An older event
that arrives later therefore never replaces a newer label, and a replayed event
can at worst leave a stale one. The label is shown and searchable in full and is
not redacted, so it must not hold personal data.
runtime says what was running when the event was recorded, and runtime.sdk
names the recorder itself: the SDK’s package name, its version, and the commit
it was built from when it knows one, for example
{ "name": "@wayscribe/node", "version": "0.1.0", "commit": "27f4d64..." }
with the full commit. It answers which services run which SDK build, which
matters most during an upgrade (F-046, ADR-063). name and version are
required inside sdk, because an sdk without them says nothing; a value over
a limit, or an empty one, is invalid_event with the path
event.runtime.sdk.<field>. The field is optional and was added in 0.1
(section 11): an event without it was recorded by an SDK from before it or by
another client, and a server from before it strips it as an unknown key and
stores the rest of runtime. The server stores runtime as the event’s
runtimeMetadata and returns it on the event read. Like every protocol field,
runtime.sdk is covered by the event’s content hash, so an event first
delivered to a server from before it (which stripped the field), whose response
was lost, and resent after that server was upgraded, is answered
event_id_conflict: it is stored, and the SDK counts it rejected. Upgrading
the server before the services, as the upgrade notes advise, never meets this.
The journey also keeps its last step: the name of the event with the latest
timestamp, under the same tie rule, so an event that arrives late never moves
it backwards, and it is the step the journey’s timeline shows last. name is required, so every event is a candidate.
A failed journey also keeps its failed step (ADR-063): the name of the
failing event that comes last in the same order among the failures applied
since the journey last became failed. A failing event is one that carries an
error or has the operation failed. So while a retry is in flight, the last
step moves on to the retry’s steps and the failed step still names the step
that failed. It is null whenever the journey is not failed: a successful retry
that clears the failure (section 5, retried) and a completed that sets the
status (section 5, completed) clear it.
Reads return it as failedStep (API_SPEC.md section 5).
4. Required field semantics
Section titled “4. Required field semantics”A globally unique, client-generated event identifier.
The API uses this field for idempotency.
Recommended format:
evt_<uuidv7>The exact prefix format is presentation guidance, not a protocol requirement.
journeyId
Section titled “journeyId”The stable identifier that joins events across processes and traces.
A journey id is an opaque string of 1 to 128 characters. The server checks
nothing about its shape. One character cannot be stored: an event whose
journey id contains a NUL is refused unstorable_payload, and the read routes
answer 404 for such an id. The Node SDK makes ids in two shapes:
- random:
jrn_and a lowercase hyphenated UUID, 40 characters, such asjrn_dd37c205-7ea6-4e14-bc8f-c07022f96696; - derived:
jrn_and 32 lowercase hex characters, 36 characters, such asjrn_5f93deccb9b599e792d560765761bec6, computed from the entity under a secret the host holds, asSDK_SPEC.mdSDK-55 says.
Another client may use any unpredictable id. A reader must not parse or
validate the shape of a journey id: both shapes above occur in one
installation, and a client in another language may make a third. The shapes
are described so that an id can be recognised in a log, not so that it can be
checked. The one check that exists is the Node SDK’s, when it reads a
propagated context (section 10): it requires the jrn_ prefix and the
characters it accepts in any propagated value, which both shapes satisfy.
A journey id must be unpredictable. A journey belongs to the environment
whose key recorded its first event, and an event for it from any other
environment is refused with journey_environment_mismatch (API_SPEC.md §3).
An id derived from business data, such as jrn_order_1001, can be guessed, and a
key for another environment of the project can record it first: every event the
rightful environment then sends for that journey is refused, and that journey is
not recorded. The Node SDK generates a random UUID for every journey it starts,
unless the host configures derived ids, which are keyed by a secret the host
holds and so cannot be guessed without it (ADR-052). An application that
chooses its own ids should do one or the other, and keep business
identifiers in entity and aliases, where they are searchable anyway.
A journey id carried across a boundary between environments is refused the same way. If a staging service propagates its context (HTTP headers, queue attributes, or a payload envelope) to a production service, the production service’s events for that journey are refused. Each environment records its own journey; start a new one where a request crosses from one environment into another.
environment
Section titled “environment”The logical deployment environment, such as:
localdevelopmentstagingproduction
The authenticated API key must be authorized for the submitted environment.
service
Section titled “service”The application component that performed the operation.
Examples:
salesforce-webhook-apicustomer-sync-workerbilling-reconciliation-job
entity
Section titled “entity”The primary business object represented by the journey.
{ "type": "customer", "id": "18492"}The ID may be an internal or external identifier. Additional representations belong in aliases.
operation
Section titled “operation”A stable semantic category used for filtering and visual display.
A developer-selected, human-readable operation name.
Examples:
transform-salesforce-accountupdate-customer-recordpublish-customer-updateddeliver-customer-to-hubspot
timestamp
Section titled “timestamp”ISO 8601 timestamp in UTC.
Example:
2026-08-06T18:31:04.120Z5. Operation semantics
Section titled “5. Operation semantics”received
Section titled “received”An input entered the observed workflow.
Examples:
- webhook accepted
- API request accepted
- file received
identified
Section titled “identified”New aliases were associated with the entity. Emitted by journey.identify().
transformed
Section titled “transformed”The shape or values of data were intentionally changed.
Normally includes both input and output.
validated
Section titled “validated”A rule or schema check was performed.
persisted
Section titled “persisted”Data was written to durable application storage.
published
Section titled “published”A message or event was sent to a queue, topic, or event bus.
consumed
Section titled “consumed”A message or event was received by a worker or subscriber.
delivered
Section titled “delivered”Data was sent to an external or downstream system.
A failed delivery attempt uses delivered (or retried for subsequent attempts),
with error populated and the HTTP status in metadata. The failed operation is
reserved for terminal journey or branch failure, such as a dead-letter transition.
See ADR-022.
failed
Section titled “failed”A processing step ended in failure.
retried
Section titled “retried”A previous operation was attempted again. Whether the attempt succeeded is in
error: a retried event carrying an error is a failed attempt, and one
carrying none is the attempt that worked.
A successful retried event clears an earlier failure, returning the
journey’s status to active, and never completes it (ADR-061). It clears
rather than completes because a journey that retried successfully and then
died without finishing must not read as completed: a falsely reassuring
status is worse than a stale alarming one. The clearing follows the same
ordering rules as any other status change, so an event stamped before the
newest one changes nothing, and a failed recorded for a terminal transition
is cleared the same way as any other failure.
completed
Section titled “completed”The journey or a major branch completed successfully. For the journey’s
status this is the only operation that sets completed, and only when it is
at or after the newest event’s timestamp; in practice it is what finish()
records.
6. Aliases
Section titled “6. Aliases”Aliases map alternate identifiers to the same logical entity.
{ "salesforceAccountId": "0018Z00002ABC", "internalCustomerId": "18492", "hubspotContactId": "9182736"}Rules:
- Alias names are developer-defined but should be stable.
- Alias values may be sensitive.
- Alias values are encrypted at rest and masked when read. An event may list
alias types in
displayableAliasesto have them shown in full; an alias is shown in full only while every event that stated it listed it, and a listed type the event’saliasesdoes not name is ignored (ADR-053). While an alias is displayable the server also stores its value in plain text, so the journey list can match it by partial text; the statement that masks the alias removes that copy, and a masked alias never has one (ADR-054). - Searchable aliases should have normalized hashes.
- Aliases must not be propagated through HTTP headers unless explicitly safe.
7. Payload capture
Section titled “7. Payload capture”input and output are optional.
Capture modes:
metadata-onlyallowlisted-fieldsredacted-payloadfull-payload
The environment policy may override SDK requests and capture less data.
The server must reject payloads exceeding configured limits.
8. Errors
Section titled “8. Errors”The error object records application evidence, not a generated diagnosis.
Stack traces may contain sensitive values and file paths. The server stores
stack only when the environment captures full payloads, and drops it in every
other mode. message, and a stored stack, are masked for credential-shaped
text before storage (SECURITY.md section 4, ADR-046).
9. Metadata
Section titled “9. Metadata”metadata is for additional structured context.
Examples:
{ "attempt": 2, "queue": "customer-updates", "httpStatus": 422, "sourceSystem": "salesforce"}Metadata keys should not replace first-class protocol fields.
10. Context propagation
Section titled “10. Context propagation”HTTP headers
Section titled “HTTP headers”x-wayscribe-journey-idx-wayscribe-entity-typex-wayscribe-entity-idtraceparentOnly non-sensitive primary identity should be propagated. Projects may disable entity headers and propagate only the journey ID.
Queue attributes
Section titled “Queue attributes”Preferred:
{ "wayscribeJourneyId": "jrn_123", "wayscribeEntityType": "customer", "wayscribeEntityId": "18492"}Fallback reserved envelope:
{ "_wayscribe": { "journeyId": "jrn_123", "entityType": "customer", "entityId": "18492" }, "data": {}}Payload mutation must be opt-in.
11. Versioning rules
Section titled “11. Versioning rules”Compatible changes
Section titled “Compatible changes”- adding optional fields
- adding optional metadata
- adding a new operation when older servers reject it clearly or treat it as unknown according to documented policy
Breaking changes
Section titled “Breaking changes”- removing fields
- changing required field meaning
- changing data types
- changing idempotency semantics
- changing timestamp semantics
Breaking changes require a new protocolVersion.
12. Validation error format
Section titled “12. Validation error format”INGESTION_CONTRACT.md section 4 is normative and lists every refusal, with its HTTP status and whether a client should send the event again; that table is checked row by row against the code. Do not read the list below as the full set, which is what it used to look like: these are the six codes this document defines, the ones about the protocol itself, and ingestion sends seven more about limits, storage and the request.
unsupported_protocol_versioninvalid_eventpayload_too_largeunauthorized_environmentevent_id_conflictjourney_environment_mismatch: the journey id belongs to another environment; a journey cannot span environments
This list once also carried missing_required_field, invalid_timestamp and invalid_operation. No code path ever sent them: a missing field, an unparseable timestamp and an operation outside the eleven are all invalid_event, with the failing field in details. They were removed rather than reserved (ADR-049). Another implementation of this protocol reports those conditions as invalid_event too.
A client treats a code it does not recognize by its HTTP status, so a code added later is a compatible change.
The full API error shape is documented in API_SPEC.md.