Skip to content

Telemetry kinds — full reference

The 22 kinds emitted on runtime.onTelemetry. Listed in the order they typically appear in a causal chain.

Domain (13)

action.dispatched

Emitted when runtime.dispatch(action, input, envelope?) enters the pipeline. Fires before the handler runs.

ts
{
  kind: "action.dispatched"
  action: string                  // ActionDefinition.name
  input: unknown                  // validated input
  envelope: MessageEnvelope
  appName: string
  ts: string                      // ISO 8601
}

action.completed

Handler returned without throwing. events published, retry loop exited successfully.

ts
{
  kind: "action.completed"
  action: string
  durationMs: number              // since action.dispatched
  emittedEvents: readonly string[]   // event names returned
  envelope: MessageEnvelope
  appName: string
  ts: string
}

action.failed

Handler threw. Emitted per retry attempt. willRetry: false on the final attempt before dlq.recorded fires.

ts
{
  kind: "action.failed"
  action: string
  attempt: number                 // 1-indexed
  maxAttempts: number
  willRetry: boolean
  error: SerializedError          // { name, message, stack?, ...custom }
  envelope: MessageEnvelope
  appName: string
  ts: string
}

event.published

An event was applied to actors + projections + reactions and committed.

ts
{
  kind: "event.published"
  event: { eventName: string; payload: unknown }
  envelope: MessageEnvelope       // derived from the publishing action's envelope
  source: "in-process" | "external"
  appName: string
  ts: string
}

source: "external" = arrived via runtime.applyExternalEvent(...) from the cross-service bus.

actor.transitioned

An actor moved between states. Fires AFTER the save commits. Pure data updates (no state change) don't fire this — only when target differs from the current state.

ts
{
  kind: "actor.transitioned"
  actor: string                   // actor.name
  key: string                     // instance key
  tenant: string
  from: string                    // previous state
  to: string                      // new state
  triggeringEvent: string         // event.eventName
  envelope: MessageEnvelope
  appName: string
  ts: string
}

projection.folded

A projection consumed an event and saved new state.

ts
{
  kind: "projection.folded"
  projection: string              // projection.name
  event: string                   // event.eventName
  tenant: string
  durationMs: number              // load + reduce + save
  envelope: MessageEnvelope
  appName: string
  ts: string
}

reaction.fired

A when() reaction's body completed successfully.

ts
{
  kind: "reaction.fired"
  sourceEvent: string             // the event that triggered it
  durationMs: number              // reaction body wall time
  envelope: MessageEnvelope
  appName: string
  ts: string
}

reaction.failed

A when() reaction threw. The publish loop exits — subsequent reactions on the same event don't run.

ts
{
  kind: "reaction.failed"
  sourceEvent: string
  error: SerializedError
  envelope: MessageEnvelope
  appName: string
  ts: string
}

query.executed

runtime.query(name, input, tenant?) ran.

ts
{
  kind: "query.executed"
  query: string                   // query.name
  input: unknown                  // validated input
  durationMs: number
  tenant: string
  appName: string
  ts: string
}

timer.scheduled

An actor entered a state with after: { timer: { delay, action } } and a timer was scheduled. Fires during applyEventToActor, AFTER the actor save commits.

ts
{
  kind: "timer.scheduled"
  actor: string
  key: string                     // actor instance key
  timer: string                   // timer name
  action: string                  // action to dispatch when due
  fireAt: number                  // epoch ms
  tenant: string
  appName: string
  ts: string
}

timer.fired

runtime.fireDueTimers() triggered a scheduled timer.

ts
{
  kind: "timer.fired"
  actor: string
  key: string
  timer: string
  action: string
  lateByMs: number                // 0 if fired on time
  tenant: string
  appName: string
  ts: string
}

dlq.recorded

Retries exhausted, entry recorded to the DLQ. Fires after the last action.failed.

ts
{
  kind: "dlq.recorded"
  action: string
  attempts: number
  error: SerializedError          // last error
  envelope: MessageEnvelope
  appName: string
  ts: string
}

Orchestrator (9)

external.call.started

ctx.externalCall(def, request) invoked. Per attempt.

ts
{
  kind: "external.call.started"
  call: string                    // ExternalCallDefinition.name
  target: string                  // "provider/endpoint"
  idempotencyKey?: string
  envelope?: MessageEnvelope      // present if called from inside a handler
  appName: string
  ts: string
}

external.call.completed

External transport returned (success).

ts
{
  kind: "external.call.completed"
  call: string
  target: string
  durationMs: number
  status?: number                 // HTTP status if applicable
  idempotencyKey?: string
  envelope?: MessageEnvelope
  appName: string
  ts: string
}

external.call.failed

External transport threw. Per attempt.

ts
{
  kind: "external.call.failed"
  call: string
  target: string
  attempt: number
  willRetry: boolean
  error: SerializedError
  envelope?: MessageEnvelope
  appName: string
  ts: string
}

inbound.webhook.received

HTTP wire handled a defineInboundWebhook request — fires per request, before action dispatch (if any).

ts
{
  kind: "inbound.webhook.received"
  webhook: string
  source: string                  // "stripe", "twilio", …
  signatureValid: boolean
  dedupHit: boolean
  routedTo?: string               // action name (if discriminator matched)
  appName: string
  ts: string
}

outbox.flushed

A batch of outbox events was flushed to the bus.

ts
{
  kind: "outbox.flushed"
  outbox: string
  events: number                  // count in batch
  durationMs: number
  failed: number                  // count that retry
  appName: string
  ts: string
}

inbox.dedup.hit

Inbound message id matched a key in the inbox window — skipped processing.

ts
{
  kind: "inbox.dedup.hit"
  inbox: string
  messageId: string
  firstSeenAt: string             // when we first processed it
  appName: string
  ts: string
}

queue.job.enqueued / .started / .completed

Queue worker lifecycle (@nwire/queue + adapter).

ts
// enqueued
{ kind: "queue.job.enqueued"; queue, jobId, delay?, appName, ts }
// started
{ kind: "queue.job.started"; queue, jobId, waitedMs, appName, ts }
// completed
{ kind: "queue.job.completed"; queue, jobId, durationMs, ok, appName, ts }

cron.fired

defineCron scheduler dispatched the bound action at its fire time.

ts
{
  kind: "cron.fired"
  schedule: string                // cron expression
  cronName: string                // CronDefinition.name
  expected: string                // ISO of expected fire time
  actual: string                  // ISO of actual dispatch
  lateByMs: number                // actual - expected
  appName: string
  ts: string
}

SerializedError

Errors are flattened before they hit the stream so the record is JSON-safe:

ts
type SerializedError = {
  name: string                    // err.name
  message: string                 // err.message
  stack?: string                  // err.stack
  [k: string]: unknown            // any enumerable own props on the error
}

Error instances → all three plus enumerable custom fields. Non-Error throws → { name: "NonError", message: String(thrown) }.

Envelope

Every domain telemetry record carries:

ts
interface MessageEnvelope {
  messageId: string               // this message's id
  correlationId: string           // chain root — same across one user request
  causationId: string             // parent message's id
  tenant?: string
  userId?: string
  timestamp: string               // ISO when envelope was minted
  version: number                 // envelope schema version (1 today)
}

Use correlationId to group records by user request (Studio's Live page does this). Use causationId to walk a chain backward.

Subscription

ts
const detach = runtime.onTelemetry((rec) => {
  // narrow on rec.kind
  if (rec.kind === "action.dispatched") {
    metrics.inc("nwire.actions", { name: rec.action })
  }
})
// later
detach()

Throwing in a listener is caught + logged; it never breaks domain dispatch. But listeners run synchronously on every emit — keep them cheap.

See also

MIT licensed.