Skip to main content
Every contract run produces one or more BoundaryLogEvents — one per attempt that fails on a retry, plus a terminal event when the run finishes. This is the shape that hits the network (or your custom write sink).
import type { BoundaryLogEvent } from "@withboundary/sdk";

Shape

BoundaryLogEvent is a tagged union on ok. Successful runs always emit a single terminal event; failed runs emit one event per non-terminal attempt plus a terminal event when retries are exhausted.
// Common base — every event carries identity + run metadata + SDK attribution.
interface BoundaryLogEventBase {
  // identity
  contractName: string;
  environment?: string;
  timestamp: string;          // ISO 8601
  runId: string;              // bnd_run_<nanoid>, stable across all events for a run

  // run metadata (always sent)
  attempt: number;
  maxAttempts: number;
  durationMs: number;
  model?: string;
  rulesCount?: number;
  schema?: SchemaField[];
  rules?: RuleDefinition[];

  // SDK attribution (always sent)
  sdk?: { name: string; version: string; runtime?: string };

  // Resolved capture policy stamped on every event
  capture?: CapturePolicy & { redactedFields?: string[] };

  // raw data — gated by capture.inputs / capture.outputs (default off)
  input?: unknown;
  output?: unknown;
}

// An attempt that satisfied schema and every rule. Always terminal.
interface AcceptedEvent extends BoundaryLogEventBase {
  ok: true;
  final: true;
}

// An attempt that didn't pass. final=false is mid-run; final=true is terminal.
interface FailedEvent extends BoundaryLogEventBase {
  ok: false;
  final: boolean;
  category: string;            // RULE_ERROR, PARSE_ERROR, REFUSAL, ...
  issues: string[];
  ruleFailures?: string[];     // names of rules that failed this attempt
  repairs?: Array<{ role: string; content: string }>;  // gated by capture.repairs
}

type BoundaryLogEvent = AcceptedEvent | FailedEvent;

Field-level reference

Identity

FieldSet byNotes
contractNameContractThe name field you passed to defineContract. Required.
environmentLoggerFrom createBoundaryLogger({ environment }). Buckets data on the dashboard.
timestampSDKISO 8601, stamped at emit time.
runIdSDKbnd_run_<nanoid>, generated once per accept() call, stamped on every event for that run. The backend coalesces per-attempt + terminal events into a single run row keyed by (organizationId, runId).

Run metadata (always sent)

FieldMeaning
attemptThe attempt this event reflects. On a non-terminal failure event it’s the attempt that just failed; on a terminal event it’s the final attempt count.
maxAttemptsFrom the contract’s retry config at the time of the run.
durationMsPer-attempt event: duration of just this attempt. Terminal event: total wall-clock across all attempts.
okDiscriminator — true on AcceptedEvent, false on FailedEvent.
finaltrue on terminal events (run is over), false on mid-run failure events. AcceptedEvent is always final: true.
modelPer-call override (contract.accept(run, { model })), else the logger’s default model, else undefined.
rulesCountNumber of rules on the contract at runtime.

Failure attribution (always sent on FailedEvent)

There is no capture.errors flag — these are structural fields that always appear when ok: false. Without them a failure event is just a flag.
FieldMeaning
categoryEMPTY_RESPONSE, REFUSAL, NO_JSON, TRUNCATED, PARSE_ERROR, VALIDATION_ERROR, RULE_ERROR, or RUN_ERROR.
issuesSchema errors or rule violations from the failed attempt.
ruleFailuresNames of the rules that failed this attempt. Maps to the dashboard’s per-rule failure attribution.
If a failure message would leak data, fix it at the source — rewrite the rule’s failure message, or use beforeSend to scrub issues before the event leaves.

Repair context

Gated by capture.repairs (default on). Present only on FailedEvent with final: false — the repair message is the prompt that’s about to be sent before the next attempt.
repairs: [
  { role: "user", content: "hot leads require score > 70, got 25" }
]

Raw data (opt-in)

FieldGate
inputcapture.inputs — off by default
outputcapture.outputs — off by default
Treat these as sensitive. See Capture policy and Redaction.

SDK attribution

Stamped automatically. Used for attribution on the dashboard and for debugging version-specific behavior. runtime is "node/<version>" on Node, "browser" where navigator.userAgent exists, or omitted on edge runtimes.

Lifecycle

Each hop runs before the event is visible on the next. If capture strips a field, it can’t be added back by beforeSend for that event.

Example success event

{
  "contractName": "invoice-extraction",
  "environment": "production",
  "timestamp": "2026-04-20T09:45:12.104Z",
  "runId": "bnd_run_8t2k7n9p4qz3v1m6h5b8x",
  "attempt": 2,
  "maxAttempts": 3,
  "durationMs": 2104,
  "model": "gpt-4.1",
  "rulesCount": 2,
  "ok": true,
  "final": true,
  "sdk": {
    "name": "@withboundary/sdk",
    "runtime": "node/22.12.0"
  }
}

Example failure event (terminal)

{
  "contractName": "agent-action-validation",
  "environment": "production",
  "timestamp": "2026-04-20T09:47:03.221Z",
  "runId": "bnd_run_q9w7e3r5t6y8u1i2o4p3a",
  "attempt": 3,
  "maxAttempts": 3,
  "durationMs": 5829,
  "model": "claude-haiku",
  "rulesCount": 4,
  "ok": false,
  "final": true,
  "category": "RULE_ERROR",
  "issues": ["cannot close a ticket with status needs_review"],
  "ruleFailures": ["cant_close_in_review"],
  "sdk": { "name": "@withboundary/sdk", "runtime": "node/22.12.0" }
}

See also

Capture policy

Three optional buckets, plus the always-on fields

createBoundaryLogger

Full options reference