Events, traces, and artifacts
Every run retains structured EventRecord objects and can stream them through
an onEvent sink. CLI, TUI, GUI, and SVG surfaces project the same facts.
Consume events
const session = await reviewWorkflow.start({prompt}, {
onEvent(event) {
console.log(JSON.stringify(event));
},
});
const failures = session.events.filter((event) =>
event.type === "step.failed" || event.type === "run.failed"
);
The callback may be asynchronous. If you provide a LoopStore, the runtime
also calls appendEvent() and saves session status as events are emitted.
Application telemetry should index at least runId, type, stepId,
attempt, timestamp, and trace identifiers.
Event record shape
type EventRecord = {
type: string;
message: string;
runId: string;
correlationId: string;
index: number;
timestamp: string;
stepId?: string;
attempt?: number;
detail?: unknown;
trace?: TraceContext;
};
index is monotonic within a run and survives snapshot hydration. Treat event
records as execution facts; do not mutate them into a second runtime state.
Run-level events use a run/event correlation identifier; step events use the
stable run:<runId>:step:<stepId>:attempt:<n> attempt correlation, which groups
facts emitted by that attempt. traceId joins the wider run trace, while
spanId identifies one traced operation.
Common event families
| Family | Examples |
|---|---|
| Run | run.started, run.completed, run.failed, run.paused, run.cancelled |
| Step | step.created, step.started, step.retrying, step.waiting, step.completed, step.failed |
| Hook | hook.created, hook.waiting, hook.resumed, hook.cancelled; delegation uses handoff_requested metadata |
| Question | question.requested, question.answered |
| Model | model.request.started, model.response.delta, model.response.completed, model.request.failed |
| Evaluation | individual judgment evidence, canonical scores, score bars, policy facts, and gate decisions |
| Composition | router selection, child lifecycle, fork branches, join, and iteration |
| Artifact | artifact.created with a registered output reference |
| Checkpoint | checkpoint.created before a step attempt |
Consumers must tolerate new event types during beta and should switch on the types they understand instead of treating unknown events as errors.
Trace ancestry
type TraceContext = {
name: string;
traceId: string;
spanId: string;
parentSpanId?: string;
status?: "unset" | "ok" | "error";
kind?: "internal" | "producer" | "consumer" | "client";
attributes?: Record<string, boolean | number | string | Array<boolean | number | string>>;
};
The run ID anchors the trace. Every step attempt receives distinct identity, so a retry does not overwrite the prior attempt. Model calls, child workflows, router routes, fork branches, and iterations retain parent ancestry.
A delegated child workflow retains child run, workflow, step, attempt, trace,
and parent identity in its handoff. Resumption attribution belongs on the
hook.resumed event source; it is not copied into workflow business state.
Workflow App thread integrations may additionally emit one deduplicated
run.suspended notification when a run first becomes waiting.
Register artifacts
const writeNote = step({
id: "notes.write",
input: {note: noteSchema},
output: {path: z.string()},
async run({artifacts, input}) {
const path = "note.json";
await Bun.write(path, `${JSON.stringify(input.note, null, 2)}\n`);
artifacts.file({mediaType: "application/json", path});
return {path};
},
});
Registration makes the file visible to Workflow App renderers. It does not write the file, upload it, or grant access; the owning step and host remain responsible for storage, redaction, retention, and authorization.
Operate from facts
Alert on terminal failures and persistence errors, not renderer labels. Use the compact renderer for routine operation and raw event records for diagnosis. Model deltas are public response progress; the SDK does not expose private reasoning. Redact sensitive input and provider output before exporting events outside the trust boundary.